Split HyperTwist review repository family
This commit is contained in:
parent
e9827c7e66
commit
c6a02c9147
5 changed files with 923 additions and 804 deletions
|
|
@ -1,5 +1,6 @@
|
|||
#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"
|
||||
|
||||
#include "HyperTwistTrainingRepositoryLibraryInternal.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingCatalogLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingKnowledgeLibrary.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingLibrary.h"
|
||||
|
|
@ -31973,807 +31974,3 @@ FHyperTwistTrainingCaseRecommendation UHyperTwistTrainingRepositoryLibrary::Make
|
|||
ReferenceUtc
|
||||
);
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewPolicy UHyperTwistTrainingRepositoryLibrary::GetDefaultReviewPolicy(
|
||||
EHyperTwistTrainingDeliveryMode DeliveryMode
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewPolicy ReviewPolicy;
|
||||
ReviewPolicy.DeliveryMode = DeliveryMode;
|
||||
ReviewPolicy.MaxCases = 5;
|
||||
ReviewPolicy.NewCaseBias = 0.0f;
|
||||
ReviewPolicy.DueCaseBias = 0.0f;
|
||||
ReviewPolicy.RecognitionBias = 0.0f;
|
||||
ReviewPolicy.AccuracyBias = 0.0f;
|
||||
ReviewPolicy.bRepeatFailuresImmediately = true;
|
||||
ReviewPolicy.bPreferShortReviewSets = false;
|
||||
|
||||
switch (DeliveryMode)
|
||||
{
|
||||
case EHyperTwistTrainingDeliveryMode::VirtualCube:
|
||||
ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::VirtualCubePractice;
|
||||
ReviewPolicy.PolicyLabel = TEXT("virtual-cube-practice");
|
||||
ReviewPolicy.MaxCases = 6;
|
||||
ReviewPolicy.NewCaseBias = 80.0f;
|
||||
ReviewPolicy.DueCaseBias = 95.0f;
|
||||
ReviewPolicy.AccuracyBias = 20.0f;
|
||||
break;
|
||||
|
||||
case EHyperTwistTrainingDeliveryMode::SmartDevice:
|
||||
ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::SmartDeviceAccuracy;
|
||||
ReviewPolicy.PolicyLabel = TEXT("smart-device-accuracy");
|
||||
ReviewPolicy.MaxCases = 5;
|
||||
ReviewPolicy.DueCaseBias = 115.0f;
|
||||
ReviewPolicy.AccuracyBias = 90.0f;
|
||||
ReviewPolicy.bPreferShortReviewSets = true;
|
||||
break;
|
||||
|
||||
case EHyperTwistTrainingDeliveryMode::RecognitionAssisted:
|
||||
ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::RecognitionAssistedScan;
|
||||
ReviewPolicy.PolicyLabel = TEXT("recognition-assisted-scan");
|
||||
ReviewPolicy.MaxCases = 4;
|
||||
ReviewPolicy.NewCaseBias = -20.0f;
|
||||
ReviewPolicy.DueCaseBias = 105.0f;
|
||||
ReviewPolicy.RecognitionBias = 130.0f;
|
||||
ReviewPolicy.AccuracyBias = 45.0f;
|
||||
ReviewPolicy.bPreferShortReviewSets = true;
|
||||
break;
|
||||
|
||||
case EHyperTwistTrainingDeliveryMode::CoachReviewed:
|
||||
ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::CoachReview;
|
||||
ReviewPolicy.PolicyLabel = TEXT("coach-review");
|
||||
ReviewPolicy.MaxCases = 4;
|
||||
ReviewPolicy.NewCaseBias = 35.0f;
|
||||
ReviewPolicy.DueCaseBias = 90.0f;
|
||||
ReviewPolicy.RecognitionBias = 30.0f;
|
||||
ReviewPolicy.AccuracyBias = 110.0f;
|
||||
ReviewPolicy.bPreferShortReviewSets = true;
|
||||
break;
|
||||
|
||||
case EHyperTwistTrainingDeliveryMode::Timer:
|
||||
default:
|
||||
ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::TimerBalanced;
|
||||
ReviewPolicy.PolicyLabel = TEXT("timer-balanced");
|
||||
ReviewPolicy.MaxCases = 5;
|
||||
ReviewPolicy.NewCaseBias = -10.0f;
|
||||
ReviewPolicy.DueCaseBias = 120.0f;
|
||||
ReviewPolicy.AccuracyBias = 35.0f;
|
||||
break;
|
||||
}
|
||||
|
||||
return ReviewPolicy;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistTrainingCaseRecommendation> UHyperTwistTrainingRepositoryLibrary::ListCaseRecommendationsForDeck(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FHyperTwistTrainingDeck& Deck,
|
||||
const FString& UserId,
|
||||
const FString& ReferenceUtc,
|
||||
int32 MaxRecommendations
|
||||
)
|
||||
{
|
||||
TArray<FHyperTwistTrainingCaseRecommendation> Recommendations;
|
||||
if (!Deck.IsStructurallyValid())
|
||||
{
|
||||
return Recommendations;
|
||||
}
|
||||
|
||||
for (const FHyperTwistTrainingCase& TrainingCase : Deck.Cases)
|
||||
{
|
||||
if (!TrainingCase.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingCaseLearnerState LearnerState;
|
||||
TryGetLearnerState(RepositoryState, UserId, Deck.DeckId, TrainingCase.CaseId, LearnerState);
|
||||
FHyperTwistTrainingCaseRecommendation Recommendation = MakeCaseRecommendation(
|
||||
TrainingCase,
|
||||
LearnerState,
|
||||
ReferenceUtc
|
||||
);
|
||||
if (Recommendation.IsStructurallyValid())
|
||||
{
|
||||
Recommendations.Add(Recommendation);
|
||||
}
|
||||
}
|
||||
|
||||
Recommendations.Sort([](const FHyperTwistTrainingCaseRecommendation& Left, const FHyperTwistTrainingCaseRecommendation& Right)
|
||||
{
|
||||
if (!FMath::IsNearlyEqual(Left.RecommendationScore, Right.RecommendationScore))
|
||||
{
|
||||
return Left.RecommendationScore > Right.RecommendationScore;
|
||||
}
|
||||
|
||||
if (!FMath::IsNearlyEqual(Left.DaysUntilDue, Right.DaysUntilDue))
|
||||
{
|
||||
return Left.DaysUntilDue < Right.DaysUntilDue;
|
||||
}
|
||||
|
||||
return Left.TrainingCase.CaseId < Right.TrainingCase.CaseId;
|
||||
});
|
||||
|
||||
if (MaxRecommendations > 0 && Recommendations.Num() > MaxRecommendations)
|
||||
{
|
||||
Recommendations.SetNum(MaxRecommendations, EAllowShrinking::No);
|
||||
}
|
||||
|
||||
return Recommendations;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistTrainingCaseRecommendation> UHyperTwistTrainingRepositoryLibrary::ApplyReviewPolicyToRecommendations(
|
||||
const TArray<FHyperTwistTrainingCaseRecommendation>& Recommendations,
|
||||
const FHyperTwistTrainingReviewPolicy& ReviewPolicy,
|
||||
int32 MaxRecommendations
|
||||
)
|
||||
{
|
||||
TArray<FHyperTwistTrainingCaseRecommendation> FilteredRecommendations;
|
||||
FilteredRecommendations.Reserve(Recommendations.Num());
|
||||
|
||||
for (const FHyperTwistTrainingCaseRecommendation& Recommendation : Recommendations)
|
||||
{
|
||||
if (!Recommendation.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingCaseRecommendation FilteredRecommendation = Recommendation;
|
||||
float AdjustedScore = FilteredRecommendation.RecommendationScore;
|
||||
|
||||
if (FilteredRecommendation.bIsNewCase)
|
||||
{
|
||||
AdjustedScore += ReviewPolicy.NewCaseBias;
|
||||
}
|
||||
if (FilteredRecommendation.bIsDue)
|
||||
{
|
||||
AdjustedScore += ReviewPolicy.DueCaseBias;
|
||||
}
|
||||
if (FilteredRecommendation.TrainingCase.PromptKind == EHyperTwistTrainingPromptKind::Recognition
|
||||
|| ReviewPolicy.DeliveryMode == EHyperTwistTrainingDeliveryMode::RecognitionAssisted)
|
||||
{
|
||||
AdjustedScore += ReviewPolicy.RecognitionBias;
|
||||
}
|
||||
if (FilteredRecommendation.bRequiresReviewFocus
|
||||
|| FilteredRecommendation.LearnerState.LastGrade == EHyperTwistTrainingReviewGrade::Again
|
||||
|| FilteredRecommendation.LearnerState.LastGrade == EHyperTwistTrainingReviewGrade::Hard
|
||||
|| FilteredRecommendation.LearnerState.LapseCount > 0)
|
||||
{
|
||||
AdjustedScore += ReviewPolicy.AccuracyBias;
|
||||
}
|
||||
|
||||
if (ReviewPolicy.bPreferShortReviewSets
|
||||
&& !FilteredRecommendation.bIsDue
|
||||
&& !FilteredRecommendation.bRequiresReviewFocus
|
||||
&& !FilteredRecommendation.bIsNewCase)
|
||||
{
|
||||
AdjustedScore -= 55.0f;
|
||||
}
|
||||
|
||||
FilteredRecommendation.RecommendationScore = AdjustedScore;
|
||||
FilteredRecommendations.Add(FilteredRecommendation);
|
||||
}
|
||||
|
||||
FilteredRecommendations.Sort([](const FHyperTwistTrainingCaseRecommendation& Left, const FHyperTwistTrainingCaseRecommendation& Right)
|
||||
{
|
||||
if (!FMath::IsNearlyEqual(Left.RecommendationScore, Right.RecommendationScore))
|
||||
{
|
||||
return Left.RecommendationScore > Right.RecommendationScore;
|
||||
}
|
||||
|
||||
if (!FMath::IsNearlyEqual(Left.DaysUntilDue, Right.DaysUntilDue))
|
||||
{
|
||||
return Left.DaysUntilDue < Right.DaysUntilDue;
|
||||
}
|
||||
|
||||
return Left.TrainingCase.CaseId < Right.TrainingCase.CaseId;
|
||||
});
|
||||
|
||||
const int32 EffectiveMaxRecommendations = HyperTwistTrainingRepositoryLibraryInternal::ResolvePolicyCaseLimit(
|
||||
ReviewPolicy,
|
||||
MaxRecommendations
|
||||
);
|
||||
if (EffectiveMaxRecommendations > 0 && FilteredRecommendations.Num() > EffectiveMaxRecommendations)
|
||||
{
|
||||
FilteredRecommendations.SetNum(EffectiveMaxRecommendations, EAllowShrinking::No);
|
||||
}
|
||||
|
||||
return FilteredRecommendations;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingDeck UHyperTwistTrainingRepositoryLibrary::BuildReviewDeckFromRecommendations(
|
||||
const FHyperTwistTrainingDeck& SourceDeck,
|
||||
const TArray<FHyperTwistTrainingCaseRecommendation>& Recommendations,
|
||||
int32 MaxCases
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingDeck ReviewDeck = SourceDeck;
|
||||
if (!SourceDeck.IsStructurallyValid())
|
||||
{
|
||||
return FHyperTwistTrainingDeck();
|
||||
}
|
||||
|
||||
ReviewDeck.DeckId = SourceDeck.DeckId + TEXT("/review");
|
||||
ReviewDeck.Title = SourceDeck.Title + TEXT(" Review");
|
||||
ReviewDeck.SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Spaced;
|
||||
ReviewDeck.Tags = SourceDeck.Tags;
|
||||
ReviewDeck.Tags.AddUnique(TEXT("review"));
|
||||
ReviewDeck.Tags.AddUnique(TEXT("recommendation-shaped"));
|
||||
ReviewDeck.Cases.Reset();
|
||||
|
||||
const int32 EffectiveMaxCases = MaxCases > 0 ? MaxCases : Recommendations.Num();
|
||||
for (const FHyperTwistTrainingCaseRecommendation& Recommendation : Recommendations)
|
||||
{
|
||||
if (!Recommendation.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ReviewDeck.Cases.Add(Recommendation.TrainingCase);
|
||||
if (ReviewDeck.Cases.Num() >= EffectiveMaxCases)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ReviewDeck.Cases.Num() == 0)
|
||||
{
|
||||
return FHyperTwistTrainingDeck();
|
||||
}
|
||||
|
||||
return ReviewDeck;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewPlanState UHyperTwistTrainingRepositoryLibrary::BuildReviewPlan(
|
||||
const FHyperTwistTrainingDeck& SourceDeck,
|
||||
const FHyperTwistTrainingDeck& ReviewDeck,
|
||||
const FString& UserId,
|
||||
const TArray<FHyperTwistTrainingCaseRecommendation>& Recommendations,
|
||||
const FHyperTwistTrainingReviewPolicy& ReviewPolicy,
|
||||
const FString& PlanId,
|
||||
const FString& ReferenceUtc,
|
||||
EHyperTwistTrainingDeliveryMode DeliveryMode
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewPlanState ReviewPlan;
|
||||
if (!SourceDeck.IsStructurallyValid() || !ReviewDeck.IsStructurallyValid() || UserId.IsEmpty())
|
||||
{
|
||||
return ReviewPlan;
|
||||
}
|
||||
|
||||
ReviewPlan.PlanId = PlanId.IsEmpty() ? FString::Printf(TEXT("review_plan_%s"), *ReviewDeck.DeckId) : PlanId;
|
||||
ReviewPlan.UserId = UserId;
|
||||
ReviewPlan.SourceDeckId = SourceDeck.DeckId;
|
||||
ReviewPlan.ReviewDeckId = ReviewDeck.DeckId;
|
||||
ReviewPlan.ReferenceUtc = ReferenceUtc.IsEmpty() ? FDateTime::UtcNow().ToIso8601() : ReferenceUtc;
|
||||
ReviewPlan.DeliveryMode = DeliveryMode;
|
||||
ReviewPlan.Policy = ReviewPolicy;
|
||||
ReviewPlan.CreatedAtUtc = ReviewPlan.ReferenceUtc;
|
||||
ReviewPlan.StartedAtUtc = ReviewPlan.ReferenceUtc;
|
||||
|
||||
for (const FHyperTwistTrainingCase& TrainingCase : ReviewDeck.Cases)
|
||||
{
|
||||
if (!TrainingCase.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingCaseRecommendation* MatchingRecommendation = Recommendations.FindByPredicate(
|
||||
[&TrainingCase](const FHyperTwistTrainingCaseRecommendation& Candidate)
|
||||
{
|
||||
return Candidate.TrainingCase.CaseId == TrainingCase.CaseId;
|
||||
}
|
||||
);
|
||||
|
||||
FHyperTwistTrainingReviewPlanEntry Entry;
|
||||
Entry.CaseId = TrainingCase.CaseId;
|
||||
Entry.PromptLabel = TrainingCase.PromptLabel;
|
||||
Entry.RecommendationReason = MatchingRecommendation != nullptr ? MatchingRecommendation->RecommendationReason : TEXT("review-case");
|
||||
Entry.RecommendationScore = MatchingRecommendation != nullptr ? MatchingRecommendation->RecommendationScore : 0.0f;
|
||||
Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::Pending;
|
||||
ReviewPlan.Entries.Add(Entry);
|
||||
}
|
||||
|
||||
if (ReviewPlan.Entries.Num() > 0)
|
||||
{
|
||||
ReviewPlan.CurrentEntryIndex = 0;
|
||||
ReviewPlan.Entries[0].EntryState = EHyperTwistTrainingReviewPlanEntryState::Current;
|
||||
}
|
||||
|
||||
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(ReviewPlan);
|
||||
}
|
||||
|
||||
FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertReviewPlan(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FHyperTwistTrainingReviewPlanState& ReviewPlan
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingRepositoryState UpdatedState = RepositoryState;
|
||||
const FHyperTwistTrainingReviewPlanState NormalizedReviewPlan =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(ReviewPlan);
|
||||
if (!NormalizedReviewPlan.IsStructurallyValid())
|
||||
{
|
||||
return UpdatedState;
|
||||
}
|
||||
|
||||
const int32 ExistingIndex = UpdatedState.ReviewPlans.IndexOfByPredicate(
|
||||
[&NormalizedReviewPlan](const FHyperTwistTrainingReviewPlanState& ExistingPlan)
|
||||
{
|
||||
return ExistingPlan.PlanId == NormalizedReviewPlan.PlanId;
|
||||
}
|
||||
);
|
||||
|
||||
if (ExistingIndex >= 0)
|
||||
{
|
||||
UpdatedState.ReviewPlans[ExistingIndex] = NormalizedReviewPlan;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdatedState.ReviewPlans.Add(NormalizedReviewPlan);
|
||||
}
|
||||
|
||||
return UpdatedState;
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingRepositoryLibrary::TryGetReviewPlan(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& PlanId,
|
||||
FHyperTwistTrainingReviewPlanState& OutReviewPlan
|
||||
)
|
||||
{
|
||||
OutReviewPlan = FHyperTwistTrainingReviewPlanState();
|
||||
if (PlanId.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingReviewPlanState* ExistingPlan = RepositoryState.ReviewPlans.FindByPredicate(
|
||||
[&PlanId](const FHyperTwistTrainingReviewPlanState& Candidate)
|
||||
{
|
||||
return Candidate.PlanId == PlanId;
|
||||
}
|
||||
);
|
||||
|
||||
if (ExistingPlan == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutReviewPlan = HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(*ExistingPlan);
|
||||
return OutReviewPlan.IsStructurallyValid();
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewPlanState UHyperTwistTrainingRepositoryLibrary::UpdateReviewPlanAfterAttempt(
|
||||
const FHyperTwistTrainingReviewPlanState& ReviewPlan,
|
||||
const FHyperTwistTrainingRunStepResult& StepResult,
|
||||
const FHyperTwistTrainingAttempt& Attempt
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewPlanState UpdatedPlan = ReviewPlan;
|
||||
if (!UpdatedPlan.IsStructurallyValid() || !StepResult.IsStructurallyValid() || !Attempt.IsStructurallyValid())
|
||||
{
|
||||
return UpdatedPlan;
|
||||
}
|
||||
|
||||
const int32 EntryIndex = UpdatedPlan.Entries.IndexOfByPredicate(
|
||||
[&Attempt](const FHyperTwistTrainingReviewPlanEntry& Entry)
|
||||
{
|
||||
return Entry.CaseId == Attempt.CaseId;
|
||||
}
|
||||
);
|
||||
if (EntryIndex < 0)
|
||||
{
|
||||
return UpdatedPlan;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewPlanEntry& Entry = UpdatedPlan.Entries[EntryIndex];
|
||||
Entry.AttemptCount += 1;
|
||||
Entry.LastAttemptId = Attempt.AttemptId;
|
||||
Entry.LastOutcome = Attempt.Result;
|
||||
Entry.LastCompletedAtUtc = Attempt.CompletedAtUtc;
|
||||
|
||||
if (Attempt.Result == EHyperTwistTrainingAttemptResult::Success)
|
||||
{
|
||||
Entry.SuccessCount += 1;
|
||||
Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::Completed;
|
||||
}
|
||||
else if (Attempt.Result == EHyperTwistTrainingAttemptResult::Aborted)
|
||||
{
|
||||
Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::Skipped;
|
||||
}
|
||||
else
|
||||
{
|
||||
Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat;
|
||||
}
|
||||
|
||||
for (int32 Index = 0; Index < UpdatedPlan.Entries.Num(); ++Index)
|
||||
{
|
||||
if (Index == EntryIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (UpdatedPlan.Entries[Index].EntryState == EHyperTwistTrainingReviewPlanEntryState::Current)
|
||||
{
|
||||
UpdatedPlan.Entries[Index].EntryState = EHyperTwistTrainingReviewPlanEntryState::Pending;
|
||||
}
|
||||
}
|
||||
|
||||
const EHyperTwistTrainingSessionState SessionState = StepResult.UpdatedRunState.Session.SessionState;
|
||||
if (SessionState == EHyperTwistTrainingSessionState::Completed
|
||||
|| SessionState == EHyperTwistTrainingSessionState::Aborted)
|
||||
{
|
||||
UpdatedPlan.bCompleted = true;
|
||||
UpdatedPlan.CurrentEntryIndex = INDEX_NONE;
|
||||
UpdatedPlan.CompletedAtUtc = !StepResult.UpdatedRunState.Session.EndedAtUtc.IsEmpty()
|
||||
? StepResult.UpdatedRunState.Session.EndedAtUtc
|
||||
: Attempt.CompletedAtUtc;
|
||||
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(UpdatedPlan);
|
||||
}
|
||||
|
||||
FString NextCaseId;
|
||||
if (StepResult.UpdatedRunState.CurrentSelection.IsStructurallyValid())
|
||||
{
|
||||
NextCaseId = StepResult.UpdatedRunState.CurrentSelection.TrainingCase.CaseId;
|
||||
}
|
||||
if (NextCaseId.IsEmpty())
|
||||
{
|
||||
NextCaseId = StepResult.UpdatedRunState.Session.CurrentCaseId;
|
||||
}
|
||||
|
||||
int32 NextEntryIndex = INDEX_NONE;
|
||||
if (!NextCaseId.IsEmpty())
|
||||
{
|
||||
NextEntryIndex = UpdatedPlan.Entries.IndexOfByPredicate(
|
||||
[&NextCaseId](const FHyperTwistTrainingReviewPlanEntry& Candidate)
|
||||
{
|
||||
return Candidate.CaseId == NextCaseId
|
||||
&& Candidate.EntryState != EHyperTwistTrainingReviewPlanEntryState::Completed
|
||||
&& Candidate.EntryState != EHyperTwistTrainingReviewPlanEntryState::Skipped;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (NextEntryIndex < 0)
|
||||
{
|
||||
NextEntryIndex = UpdatedPlan.Entries.IndexOfByPredicate(
|
||||
[](const FHyperTwistTrainingReviewPlanEntry& Candidate)
|
||||
{
|
||||
return Candidate.EntryState == EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat
|
||||
|| Candidate.EntryState == EHyperTwistTrainingReviewPlanEntryState::Pending;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
UpdatedPlan.CurrentEntryIndex = NextEntryIndex;
|
||||
if (NextEntryIndex >= 0)
|
||||
{
|
||||
UpdatedPlan.Entries[NextEntryIndex].EntryState = EHyperTwistTrainingReviewPlanEntryState::Current;
|
||||
}
|
||||
|
||||
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(UpdatedPlan);
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewFlowStatus UHyperTwistTrainingRepositoryLibrary::DeriveReviewFlowStatus(
|
||||
const FHyperTwistTrainingReviewPlanState& ReviewPlan
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewFlowStatus FlowStatus;
|
||||
if (!ReviewPlan.IsStructurallyValid())
|
||||
{
|
||||
return FlowStatus;
|
||||
}
|
||||
|
||||
FlowStatus.PlanId = ReviewPlan.PlanId;
|
||||
FlowStatus.bPlanComplete = ReviewPlan.bCompleted;
|
||||
|
||||
int32 CurrentEntryIndex = ReviewPlan.CurrentEntryIndex;
|
||||
if (!ReviewPlan.Entries.IsValidIndex(CurrentEntryIndex))
|
||||
{
|
||||
CurrentEntryIndex = ReviewPlan.Entries.IndexOfByPredicate(
|
||||
[](const FHyperTwistTrainingReviewPlanEntry& Entry)
|
||||
{
|
||||
return Entry.EntryState == EHyperTwistTrainingReviewPlanEntryState::Current;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (const FHyperTwistTrainingReviewPlanEntry& Entry : ReviewPlan.Entries)
|
||||
{
|
||||
switch (Entry.EntryState)
|
||||
{
|
||||
case EHyperTwistTrainingReviewPlanEntryState::Pending:
|
||||
FlowStatus.PendingCount += 1;
|
||||
break;
|
||||
case EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat:
|
||||
FlowStatus.RepeatCount += 1;
|
||||
break;
|
||||
case EHyperTwistTrainingReviewPlanEntryState::Completed:
|
||||
FlowStatus.CompletedCount += 1;
|
||||
break;
|
||||
case EHyperTwistTrainingReviewPlanEntryState::Skipped:
|
||||
FlowStatus.SkippedCount += 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (Entry.EntryState == EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat && Entry.AttemptCount >= 2)
|
||||
{
|
||||
FlowStatus.bNeedsCoachIntervention = true;
|
||||
}
|
||||
}
|
||||
|
||||
const bool bCoachFacingRepeatPressure =
|
||||
FlowStatus.RepeatCount > 0
|
||||
&& (
|
||||
ReviewPlan.Policy.PolicyKind == EHyperTwistTrainingReviewPolicyKind::CoachReview
|
||||
|| ReviewPlan.Policy.bPreferRecognitionReplayReview
|
||||
);
|
||||
if (bCoachFacingRepeatPressure)
|
||||
{
|
||||
FlowStatus.bNeedsCoachIntervention = true;
|
||||
}
|
||||
|
||||
const int32 TotalTerminalCount = FlowStatus.CompletedCount + FlowStatus.SkippedCount;
|
||||
if (ReviewPlan.Entries.Num() > 0)
|
||||
{
|
||||
FlowStatus.ProgressRatio = static_cast<float>(TotalTerminalCount) / static_cast<float>(ReviewPlan.Entries.Num());
|
||||
}
|
||||
|
||||
if (ReviewPlan.Entries.IsValidIndex(CurrentEntryIndex))
|
||||
{
|
||||
const FHyperTwistTrainingReviewPlanEntry& CurrentEntry = ReviewPlan.Entries[CurrentEntryIndex];
|
||||
FlowStatus.CurrentCaseId = CurrentEntry.CaseId;
|
||||
FlowStatus.CurrentPromptLabel = CurrentEntry.PromptLabel;
|
||||
FlowStatus.bHasCurrentEntry = true;
|
||||
}
|
||||
|
||||
if (FlowStatus.bPlanComplete || TotalTerminalCount >= ReviewPlan.Entries.Num())
|
||||
{
|
||||
FlowStatus.bPlanComplete = true;
|
||||
FlowStatus.NextActionLabel = TEXT("review-complete");
|
||||
}
|
||||
else if (FlowStatus.bNeedsCoachIntervention)
|
||||
{
|
||||
FlowStatus.NextActionLabel = TEXT("coach-review-needed");
|
||||
}
|
||||
else if (FlowStatus.bHasCurrentEntry)
|
||||
{
|
||||
FlowStatus.NextActionLabel = TEXT("continue-review");
|
||||
}
|
||||
else if (FlowStatus.RepeatCount > 0)
|
||||
{
|
||||
FlowStatus.NextActionLabel = TEXT("repeat-failed-cases");
|
||||
}
|
||||
else if (FlowStatus.PendingCount > 0)
|
||||
{
|
||||
FlowStatus.NextActionLabel = TEXT("advance-to-next-case");
|
||||
}
|
||||
else
|
||||
{
|
||||
FlowStatus.NextActionLabel = TEXT("finalize-review");
|
||||
}
|
||||
|
||||
return FlowStatus;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewProgramSummary UHyperTwistTrainingRepositoryLibrary::DeriveReviewProgramSummary(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& UserId,
|
||||
const FString& ReferenceUtc
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewProgramSummary Summary;
|
||||
if (UserId.IsEmpty())
|
||||
{
|
||||
return Summary;
|
||||
}
|
||||
|
||||
Summary.UserId = UserId;
|
||||
Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc);
|
||||
|
||||
const TArray<FHyperTwistTrainingMethodSegment> MethodSegments = DeriveLearnerMethodSegments(
|
||||
RepositoryState,
|
||||
UserId,
|
||||
Summary.ReferenceUtc
|
||||
);
|
||||
const auto ResolveMethodSegmentIdForDeck = [&MethodSegments](const FString& DeckId) -> FString
|
||||
{
|
||||
for (const FHyperTwistTrainingMethodSegment& MethodSegment : MethodSegments)
|
||||
{
|
||||
if (MethodSegment.DeckIds.Contains(DeckId))
|
||||
{
|
||||
return MethodSegment.MethodDescriptor.SegmentId;
|
||||
}
|
||||
}
|
||||
|
||||
return FString();
|
||||
};
|
||||
|
||||
int32 BestCarryoverScore = -1;
|
||||
FString BestPlanCreatedAtUtc;
|
||||
FString LatestCompletedAtUtc;
|
||||
int32 DeliveryModeCounts[5] = {0, 0, 0, 0, 0};
|
||||
TMap<EHyperTwistTrainingReviewPolicyKind, int32> PolicyCounts;
|
||||
int64 CompletedPlanDurationAccumulator = 0;
|
||||
int32 CompletedPlanDurationCount = 0;
|
||||
float ProgressRatioAccumulator = 0.0f;
|
||||
|
||||
for (const FHyperTwistTrainingReviewPlanState& ReviewPlan : RepositoryState.ReviewPlans)
|
||||
{
|
||||
if (!ReviewPlan.IsStructurallyValid() || ReviewPlan.UserId != UserId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingReviewFlowStatus FlowStatus = DeriveReviewFlowStatus(ReviewPlan);
|
||||
const int32 CarryoverCount = FlowStatus.PendingCount
|
||||
+ FlowStatus.RepeatCount
|
||||
+ (FlowStatus.bHasCurrentEntry ? 1 : 0);
|
||||
const int32 DeliveryModeIndex = FMath::Clamp(static_cast<int32>(ReviewPlan.DeliveryMode), 0, 4);
|
||||
DeliveryModeCounts[DeliveryModeIndex] += 1;
|
||||
PolicyCounts.FindOrAdd(ReviewPlan.Policy.PolicyKind) += 1;
|
||||
Summary.TotalPlanCount += 1;
|
||||
Summary.TotalEntryCount += ReviewPlan.Entries.Num();
|
||||
Summary.CompletedEntryCount += FlowStatus.CompletedCount;
|
||||
Summary.SkippedEntryCount += FlowStatus.SkippedCount;
|
||||
ProgressRatioAccumulator += FlowStatus.ProgressRatio;
|
||||
|
||||
if (Summary.LastPlanCreatedAtUtc.IsEmpty()
|
||||
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
|
||||
ReviewPlan.CreatedAtUtc,
|
||||
Summary.LastPlanCreatedAtUtc
|
||||
) > 0)
|
||||
{
|
||||
Summary.LastPlanCreatedAtUtc = ReviewPlan.CreatedAtUtc;
|
||||
}
|
||||
|
||||
if (ReviewPlan.bCompleted || FlowStatus.bPlanComplete)
|
||||
{
|
||||
Summary.CompletedPlanCount += 1;
|
||||
const FString CompletedAtUtc = !ReviewPlan.CompletedAtUtc.IsEmpty()
|
||||
? ReviewPlan.CompletedAtUtc
|
||||
: ReviewPlan.CreatedAtUtc;
|
||||
if (LatestCompletedAtUtc.IsEmpty()
|
||||
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(CompletedAtUtc, LatestCompletedAtUtc) > 0)
|
||||
{
|
||||
LatestCompletedAtUtc = CompletedAtUtc;
|
||||
Summary.LastCompletedPlanId = ReviewPlan.PlanId;
|
||||
}
|
||||
const int32 PlanDurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputePlanDurationMs(
|
||||
ReviewPlan,
|
||||
Summary.ReferenceUtc
|
||||
);
|
||||
if (PlanDurationMs > 0)
|
||||
{
|
||||
CompletedPlanDurationAccumulator += PlanDurationMs;
|
||||
CompletedPlanDurationCount += 1;
|
||||
if (Summary.BestCompletedPlanDurationMs == 0 || PlanDurationMs < Summary.BestCompletedPlanDurationMs)
|
||||
{
|
||||
Summary.BestCompletedPlanDurationMs = PlanDurationMs;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Summary.PendingPlanIds.Add(ReviewPlan.PlanId);
|
||||
Summary.PendingPlanCount += 1;
|
||||
Summary.PendingEntryCount += FlowStatus.PendingCount;
|
||||
Summary.RepeatEntryCount += FlowStatus.RepeatCount;
|
||||
Summary.CarryoverCaseCount += CarryoverCount;
|
||||
if (FlowStatus.bNeedsCoachIntervention)
|
||||
{
|
||||
Summary.CoachInterventionPlanCount += 1;
|
||||
Summary.bNeedsCoachIntervention = true;
|
||||
}
|
||||
|
||||
const int32 CarryoverScore = CarryoverCount * 100
|
||||
+ (FlowStatus.bNeedsCoachIntervention ? 25 : 0)
|
||||
+ (FlowStatus.bHasCurrentEntry ? 10 : 0);
|
||||
if (CarryoverScore > BestCarryoverScore
|
||||
|| (CarryoverScore == BestCarryoverScore
|
||||
&& (BestPlanCreatedAtUtc.IsEmpty()
|
||||
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(ReviewPlan.CreatedAtUtc, BestPlanCreatedAtUtc) > 0)))
|
||||
{
|
||||
BestCarryoverScore = CarryoverScore;
|
||||
BestPlanCreatedAtUtc = ReviewPlan.CreatedAtUtc;
|
||||
Summary.FocusDeckId = ReviewPlan.SourceDeckId;
|
||||
Summary.FocusMethodSegmentId = ResolveMethodSegmentIdForDeck(ReviewPlan.SourceDeckId);
|
||||
Summary.NextPlanId = ReviewPlan.PlanId;
|
||||
if (FlowStatus.bHasCurrentEntry)
|
||||
{
|
||||
Summary.CurrentPlanId = ReviewPlan.PlanId;
|
||||
}
|
||||
}
|
||||
|
||||
if (Summary.CurrentPlanId.IsEmpty() && FlowStatus.bHasCurrentEntry)
|
||||
{
|
||||
Summary.CurrentPlanId = ReviewPlan.PlanId;
|
||||
}
|
||||
}
|
||||
|
||||
if (Summary.CurrentPlanId.IsEmpty())
|
||||
{
|
||||
Summary.CurrentPlanId = Summary.NextPlanId;
|
||||
}
|
||||
if (Summary.FocusMethodSegmentId.IsEmpty() && !Summary.FocusDeckId.IsEmpty())
|
||||
{
|
||||
Summary.FocusMethodSegmentId = ResolveMethodSegmentIdForDeck(Summary.FocusDeckId);
|
||||
}
|
||||
|
||||
if (Summary.TotalPlanCount > 0)
|
||||
{
|
||||
Summary.AveragePlanProgressRatio = ProgressRatioAccumulator / static_cast<float>(Summary.TotalPlanCount);
|
||||
}
|
||||
if (Summary.PendingPlanCount > 0)
|
||||
{
|
||||
Summary.AveragePendingEntriesPerOpenPlan =
|
||||
static_cast<float>(Summary.PendingEntryCount) / static_cast<float>(Summary.PendingPlanCount);
|
||||
Summary.AverageCarryoverCasesPerOpenPlan =
|
||||
static_cast<float>(Summary.CarryoverCaseCount) / static_cast<float>(Summary.PendingPlanCount);
|
||||
}
|
||||
if (CompletedPlanDurationCount > 0)
|
||||
{
|
||||
Summary.AverageCompletedPlanDurationMs = static_cast<int32>(
|
||||
CompletedPlanDurationAccumulator / CompletedPlanDurationCount
|
||||
);
|
||||
}
|
||||
|
||||
int32 BestModeCount = -1;
|
||||
for (int32 ModeIndex = 0; ModeIndex < UE_ARRAY_COUNT(DeliveryModeCounts); ++ModeIndex)
|
||||
{
|
||||
if (DeliveryModeCounts[ModeIndex] > BestModeCount)
|
||||
{
|
||||
BestModeCount = DeliveryModeCounts[ModeIndex];
|
||||
Summary.DominantDeliveryMode = static_cast<EHyperTwistTrainingDeliveryMode>(ModeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
int32 BestPolicyCount = -1;
|
||||
for (const TPair<EHyperTwistTrainingReviewPolicyKind, int32>& Pair : PolicyCounts)
|
||||
{
|
||||
if (Pair.Value > BestPolicyCount)
|
||||
{
|
||||
BestPolicyCount = Pair.Value;
|
||||
Summary.DominantPolicyKind = Pair.Key;
|
||||
}
|
||||
}
|
||||
|
||||
Summary.ProgramPressureScore =
|
||||
static_cast<float>(Summary.CarryoverCaseCount * 1.0f)
|
||||
+ static_cast<float>(Summary.RepeatEntryCount * 1.5f)
|
||||
+ static_cast<float>(Summary.CoachInterventionPlanCount * 3.0f)
|
||||
+ (Summary.CurrentPlanId.IsEmpty() ? 0.0f : 1.0f);
|
||||
Summary.bHasCarryover = Summary.CarryoverCaseCount > 0 || Summary.PendingPlanCount > 0;
|
||||
return Summary;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewPlanState UHyperTwistTrainingRepositoryLibrary::FinalizeReviewPlan(
|
||||
const FHyperTwistTrainingReviewPlanState& ReviewPlan,
|
||||
const FHyperTwistTrainingSession& Session
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewPlanState UpdatedPlan = ReviewPlan;
|
||||
if (!UpdatedPlan.IsStructurallyValid())
|
||||
{
|
||||
return UpdatedPlan;
|
||||
}
|
||||
|
||||
UpdatedPlan.bCompleted = false;
|
||||
UpdatedPlan.CurrentEntryIndex = INDEX_NONE;
|
||||
UpdatedPlan.CompletedAtUtc = !Session.EndedAtUtc.IsEmpty()
|
||||
? Session.EndedAtUtc
|
||||
: FDateTime::UtcNow().ToIso8601();
|
||||
|
||||
for (FHyperTwistTrainingReviewPlanEntry& Entry : UpdatedPlan.Entries)
|
||||
{
|
||||
if (Entry.EntryState == EHyperTwistTrainingReviewPlanEntryState::Current)
|
||||
{
|
||||
Entry.EntryState = Session.SessionState == EHyperTwistTrainingSessionState::Aborted
|
||||
? EHyperTwistTrainingReviewPlanEntryState::Skipped
|
||||
: EHyperTwistTrainingReviewPlanEntryState::Pending;
|
||||
}
|
||||
}
|
||||
|
||||
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(UpdatedPlan);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright HyperTwist, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"
|
||||
|
||||
namespace HyperTwistTrainingRepositoryLibraryInternal
|
||||
{
|
||||
int32 CompareUtcStrings(const FString& Left, const FString& Right);
|
||||
|
||||
FString ResolveReferenceUtc(const FString& ReferenceUtc);
|
||||
|
||||
int32 ComputePlanDurationMs(
|
||||
const FHyperTwistTrainingReviewPlanState& ReviewPlan,
|
||||
const FString& ReferenceUtc
|
||||
);
|
||||
|
||||
FHyperTwistTrainingReviewPlanState NormalizeReviewPlanState(
|
||||
const FHyperTwistTrainingReviewPlanState& ReviewPlan
|
||||
);
|
||||
|
||||
int32 ResolvePolicyCaseLimit(
|
||||
const FHyperTwistTrainingReviewPolicy& ReviewPolicy,
|
||||
int32 RequestedMaxCases
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,809 @@
|
|||
// Copyright HyperTwist, Inc. All Rights Reserved.
|
||||
|
||||
#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"
|
||||
|
||||
#include "HyperTwistTrainingRepositoryLibraryInternal.h"
|
||||
|
||||
FHyperTwistTrainingReviewPolicy UHyperTwistTrainingRepositoryLibrary::GetDefaultReviewPolicy(
|
||||
EHyperTwistTrainingDeliveryMode DeliveryMode
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewPolicy ReviewPolicy;
|
||||
ReviewPolicy.DeliveryMode = DeliveryMode;
|
||||
ReviewPolicy.MaxCases = 5;
|
||||
ReviewPolicy.NewCaseBias = 0.0f;
|
||||
ReviewPolicy.DueCaseBias = 0.0f;
|
||||
ReviewPolicy.RecognitionBias = 0.0f;
|
||||
ReviewPolicy.AccuracyBias = 0.0f;
|
||||
ReviewPolicy.bRepeatFailuresImmediately = true;
|
||||
ReviewPolicy.bPreferShortReviewSets = false;
|
||||
|
||||
switch (DeliveryMode)
|
||||
{
|
||||
case EHyperTwistTrainingDeliveryMode::VirtualCube:
|
||||
ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::VirtualCubePractice;
|
||||
ReviewPolicy.PolicyLabel = TEXT("virtual-cube-practice");
|
||||
ReviewPolicy.MaxCases = 6;
|
||||
ReviewPolicy.NewCaseBias = 80.0f;
|
||||
ReviewPolicy.DueCaseBias = 95.0f;
|
||||
ReviewPolicy.AccuracyBias = 20.0f;
|
||||
break;
|
||||
|
||||
case EHyperTwistTrainingDeliveryMode::SmartDevice:
|
||||
ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::SmartDeviceAccuracy;
|
||||
ReviewPolicy.PolicyLabel = TEXT("smart-device-accuracy");
|
||||
ReviewPolicy.MaxCases = 5;
|
||||
ReviewPolicy.DueCaseBias = 115.0f;
|
||||
ReviewPolicy.AccuracyBias = 90.0f;
|
||||
ReviewPolicy.bPreferShortReviewSets = true;
|
||||
break;
|
||||
|
||||
case EHyperTwistTrainingDeliveryMode::RecognitionAssisted:
|
||||
ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::RecognitionAssistedScan;
|
||||
ReviewPolicy.PolicyLabel = TEXT("recognition-assisted-scan");
|
||||
ReviewPolicy.MaxCases = 4;
|
||||
ReviewPolicy.NewCaseBias = -20.0f;
|
||||
ReviewPolicy.DueCaseBias = 105.0f;
|
||||
ReviewPolicy.RecognitionBias = 130.0f;
|
||||
ReviewPolicy.AccuracyBias = 45.0f;
|
||||
ReviewPolicy.bPreferShortReviewSets = true;
|
||||
break;
|
||||
|
||||
case EHyperTwistTrainingDeliveryMode::CoachReviewed:
|
||||
ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::CoachReview;
|
||||
ReviewPolicy.PolicyLabel = TEXT("coach-review");
|
||||
ReviewPolicy.MaxCases = 4;
|
||||
ReviewPolicy.NewCaseBias = 35.0f;
|
||||
ReviewPolicy.DueCaseBias = 90.0f;
|
||||
ReviewPolicy.RecognitionBias = 30.0f;
|
||||
ReviewPolicy.AccuracyBias = 110.0f;
|
||||
ReviewPolicy.bPreferShortReviewSets = true;
|
||||
break;
|
||||
|
||||
case EHyperTwistTrainingDeliveryMode::Timer:
|
||||
default:
|
||||
ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::TimerBalanced;
|
||||
ReviewPolicy.PolicyLabel = TEXT("timer-balanced");
|
||||
ReviewPolicy.MaxCases = 5;
|
||||
ReviewPolicy.NewCaseBias = -10.0f;
|
||||
ReviewPolicy.DueCaseBias = 120.0f;
|
||||
ReviewPolicy.AccuracyBias = 35.0f;
|
||||
break;
|
||||
}
|
||||
|
||||
return ReviewPolicy;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistTrainingCaseRecommendation> UHyperTwistTrainingRepositoryLibrary::ListCaseRecommendationsForDeck(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FHyperTwistTrainingDeck& Deck,
|
||||
const FString& UserId,
|
||||
const FString& ReferenceUtc,
|
||||
int32 MaxRecommendations
|
||||
)
|
||||
{
|
||||
TArray<FHyperTwistTrainingCaseRecommendation> Recommendations;
|
||||
if (!Deck.IsStructurallyValid())
|
||||
{
|
||||
return Recommendations;
|
||||
}
|
||||
|
||||
for (const FHyperTwistTrainingCase& TrainingCase : Deck.Cases)
|
||||
{
|
||||
if (!TrainingCase.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingCaseLearnerState LearnerState;
|
||||
TryGetLearnerState(RepositoryState, UserId, Deck.DeckId, TrainingCase.CaseId, LearnerState);
|
||||
FHyperTwistTrainingCaseRecommendation Recommendation = MakeCaseRecommendation(
|
||||
TrainingCase,
|
||||
LearnerState,
|
||||
ReferenceUtc
|
||||
);
|
||||
if (Recommendation.IsStructurallyValid())
|
||||
{
|
||||
Recommendations.Add(Recommendation);
|
||||
}
|
||||
}
|
||||
|
||||
Recommendations.Sort([](const FHyperTwistTrainingCaseRecommendation& Left, const FHyperTwistTrainingCaseRecommendation& Right)
|
||||
{
|
||||
if (!FMath::IsNearlyEqual(Left.RecommendationScore, Right.RecommendationScore))
|
||||
{
|
||||
return Left.RecommendationScore > Right.RecommendationScore;
|
||||
}
|
||||
|
||||
if (!FMath::IsNearlyEqual(Left.DaysUntilDue, Right.DaysUntilDue))
|
||||
{
|
||||
return Left.DaysUntilDue < Right.DaysUntilDue;
|
||||
}
|
||||
|
||||
return Left.TrainingCase.CaseId < Right.TrainingCase.CaseId;
|
||||
});
|
||||
|
||||
if (MaxRecommendations > 0 && Recommendations.Num() > MaxRecommendations)
|
||||
{
|
||||
Recommendations.SetNum(MaxRecommendations, EAllowShrinking::No);
|
||||
}
|
||||
|
||||
return Recommendations;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistTrainingCaseRecommendation> UHyperTwistTrainingRepositoryLibrary::ApplyReviewPolicyToRecommendations(
|
||||
const TArray<FHyperTwistTrainingCaseRecommendation>& Recommendations,
|
||||
const FHyperTwistTrainingReviewPolicy& ReviewPolicy,
|
||||
int32 MaxRecommendations
|
||||
)
|
||||
{
|
||||
TArray<FHyperTwistTrainingCaseRecommendation> FilteredRecommendations;
|
||||
FilteredRecommendations.Reserve(Recommendations.Num());
|
||||
|
||||
for (const FHyperTwistTrainingCaseRecommendation& Recommendation : Recommendations)
|
||||
{
|
||||
if (!Recommendation.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingCaseRecommendation FilteredRecommendation = Recommendation;
|
||||
float AdjustedScore = FilteredRecommendation.RecommendationScore;
|
||||
|
||||
if (FilteredRecommendation.bIsNewCase)
|
||||
{
|
||||
AdjustedScore += ReviewPolicy.NewCaseBias;
|
||||
}
|
||||
if (FilteredRecommendation.bIsDue)
|
||||
{
|
||||
AdjustedScore += ReviewPolicy.DueCaseBias;
|
||||
}
|
||||
if (FilteredRecommendation.TrainingCase.PromptKind == EHyperTwistTrainingPromptKind::Recognition
|
||||
|| ReviewPolicy.DeliveryMode == EHyperTwistTrainingDeliveryMode::RecognitionAssisted)
|
||||
{
|
||||
AdjustedScore += ReviewPolicy.RecognitionBias;
|
||||
}
|
||||
if (FilteredRecommendation.bRequiresReviewFocus
|
||||
|| FilteredRecommendation.LearnerState.LastGrade == EHyperTwistTrainingReviewGrade::Again
|
||||
|| FilteredRecommendation.LearnerState.LastGrade == EHyperTwistTrainingReviewGrade::Hard
|
||||
|| FilteredRecommendation.LearnerState.LapseCount > 0)
|
||||
{
|
||||
AdjustedScore += ReviewPolicy.AccuracyBias;
|
||||
}
|
||||
|
||||
if (ReviewPolicy.bPreferShortReviewSets
|
||||
&& !FilteredRecommendation.bIsDue
|
||||
&& !FilteredRecommendation.bRequiresReviewFocus
|
||||
&& !FilteredRecommendation.bIsNewCase)
|
||||
{
|
||||
AdjustedScore -= 55.0f;
|
||||
}
|
||||
|
||||
FilteredRecommendation.RecommendationScore = AdjustedScore;
|
||||
FilteredRecommendations.Add(FilteredRecommendation);
|
||||
}
|
||||
|
||||
FilteredRecommendations.Sort([](const FHyperTwistTrainingCaseRecommendation& Left, const FHyperTwistTrainingCaseRecommendation& Right)
|
||||
{
|
||||
if (!FMath::IsNearlyEqual(Left.RecommendationScore, Right.RecommendationScore))
|
||||
{
|
||||
return Left.RecommendationScore > Right.RecommendationScore;
|
||||
}
|
||||
|
||||
if (!FMath::IsNearlyEqual(Left.DaysUntilDue, Right.DaysUntilDue))
|
||||
{
|
||||
return Left.DaysUntilDue < Right.DaysUntilDue;
|
||||
}
|
||||
|
||||
return Left.TrainingCase.CaseId < Right.TrainingCase.CaseId;
|
||||
});
|
||||
|
||||
const int32 EffectiveMaxRecommendations = HyperTwistTrainingRepositoryLibraryInternal::ResolvePolicyCaseLimit(
|
||||
ReviewPolicy,
|
||||
MaxRecommendations
|
||||
);
|
||||
if (EffectiveMaxRecommendations > 0 && FilteredRecommendations.Num() > EffectiveMaxRecommendations)
|
||||
{
|
||||
FilteredRecommendations.SetNum(EffectiveMaxRecommendations, EAllowShrinking::No);
|
||||
}
|
||||
|
||||
return FilteredRecommendations;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingDeck UHyperTwistTrainingRepositoryLibrary::BuildReviewDeckFromRecommendations(
|
||||
const FHyperTwistTrainingDeck& SourceDeck,
|
||||
const TArray<FHyperTwistTrainingCaseRecommendation>& Recommendations,
|
||||
int32 MaxCases
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingDeck ReviewDeck = SourceDeck;
|
||||
if (!SourceDeck.IsStructurallyValid())
|
||||
{
|
||||
return FHyperTwistTrainingDeck();
|
||||
}
|
||||
|
||||
ReviewDeck.DeckId = SourceDeck.DeckId + TEXT("/review");
|
||||
ReviewDeck.Title = SourceDeck.Title + TEXT(" Review");
|
||||
ReviewDeck.SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Spaced;
|
||||
ReviewDeck.Tags = SourceDeck.Tags;
|
||||
ReviewDeck.Tags.AddUnique(TEXT("review"));
|
||||
ReviewDeck.Tags.AddUnique(TEXT("recommendation-shaped"));
|
||||
ReviewDeck.Cases.Reset();
|
||||
|
||||
const int32 EffectiveMaxCases = MaxCases > 0 ? MaxCases : Recommendations.Num();
|
||||
for (const FHyperTwistTrainingCaseRecommendation& Recommendation : Recommendations)
|
||||
{
|
||||
if (!Recommendation.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ReviewDeck.Cases.Add(Recommendation.TrainingCase);
|
||||
if (ReviewDeck.Cases.Num() >= EffectiveMaxCases)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ReviewDeck.Cases.Num() == 0)
|
||||
{
|
||||
return FHyperTwistTrainingDeck();
|
||||
}
|
||||
|
||||
return ReviewDeck;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewPlanState UHyperTwistTrainingRepositoryLibrary::BuildReviewPlan(
|
||||
const FHyperTwistTrainingDeck& SourceDeck,
|
||||
const FHyperTwistTrainingDeck& ReviewDeck,
|
||||
const FString& UserId,
|
||||
const TArray<FHyperTwistTrainingCaseRecommendation>& Recommendations,
|
||||
const FHyperTwistTrainingReviewPolicy& ReviewPolicy,
|
||||
const FString& PlanId,
|
||||
const FString& ReferenceUtc,
|
||||
EHyperTwistTrainingDeliveryMode DeliveryMode
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewPlanState ReviewPlan;
|
||||
if (!SourceDeck.IsStructurallyValid() || !ReviewDeck.IsStructurallyValid() || UserId.IsEmpty())
|
||||
{
|
||||
return ReviewPlan;
|
||||
}
|
||||
|
||||
ReviewPlan.PlanId = PlanId.IsEmpty() ? FString::Printf(TEXT("review_plan_%s"), *ReviewDeck.DeckId) : PlanId;
|
||||
ReviewPlan.UserId = UserId;
|
||||
ReviewPlan.SourceDeckId = SourceDeck.DeckId;
|
||||
ReviewPlan.ReviewDeckId = ReviewDeck.DeckId;
|
||||
ReviewPlan.ReferenceUtc = ReferenceUtc.IsEmpty() ? FDateTime::UtcNow().ToIso8601() : ReferenceUtc;
|
||||
ReviewPlan.DeliveryMode = DeliveryMode;
|
||||
ReviewPlan.Policy = ReviewPolicy;
|
||||
ReviewPlan.CreatedAtUtc = ReviewPlan.ReferenceUtc;
|
||||
ReviewPlan.StartedAtUtc = ReviewPlan.ReferenceUtc;
|
||||
|
||||
for (const FHyperTwistTrainingCase& TrainingCase : ReviewDeck.Cases)
|
||||
{
|
||||
if (!TrainingCase.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingCaseRecommendation* MatchingRecommendation = Recommendations.FindByPredicate(
|
||||
[&TrainingCase](const FHyperTwistTrainingCaseRecommendation& Candidate)
|
||||
{
|
||||
return Candidate.TrainingCase.CaseId == TrainingCase.CaseId;
|
||||
}
|
||||
);
|
||||
|
||||
FHyperTwistTrainingReviewPlanEntry Entry;
|
||||
Entry.CaseId = TrainingCase.CaseId;
|
||||
Entry.PromptLabel = TrainingCase.PromptLabel;
|
||||
Entry.RecommendationReason = MatchingRecommendation != nullptr ? MatchingRecommendation->RecommendationReason : TEXT("review-case");
|
||||
Entry.RecommendationScore = MatchingRecommendation != nullptr ? MatchingRecommendation->RecommendationScore : 0.0f;
|
||||
Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::Pending;
|
||||
ReviewPlan.Entries.Add(Entry);
|
||||
}
|
||||
|
||||
if (ReviewPlan.Entries.Num() > 0)
|
||||
{
|
||||
ReviewPlan.CurrentEntryIndex = 0;
|
||||
ReviewPlan.Entries[0].EntryState = EHyperTwistTrainingReviewPlanEntryState::Current;
|
||||
}
|
||||
|
||||
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(ReviewPlan);
|
||||
}
|
||||
|
||||
FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertReviewPlan(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FHyperTwistTrainingReviewPlanState& ReviewPlan
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingRepositoryState UpdatedState = RepositoryState;
|
||||
const FHyperTwistTrainingReviewPlanState NormalizedReviewPlan =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(ReviewPlan);
|
||||
if (!NormalizedReviewPlan.IsStructurallyValid())
|
||||
{
|
||||
return UpdatedState;
|
||||
}
|
||||
|
||||
const int32 ExistingIndex = UpdatedState.ReviewPlans.IndexOfByPredicate(
|
||||
[&NormalizedReviewPlan](const FHyperTwistTrainingReviewPlanState& ExistingPlan)
|
||||
{
|
||||
return ExistingPlan.PlanId == NormalizedReviewPlan.PlanId;
|
||||
}
|
||||
);
|
||||
|
||||
if (ExistingIndex >= 0)
|
||||
{
|
||||
UpdatedState.ReviewPlans[ExistingIndex] = NormalizedReviewPlan;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdatedState.ReviewPlans.Add(NormalizedReviewPlan);
|
||||
}
|
||||
|
||||
return UpdatedState;
|
||||
}
|
||||
|
||||
bool UHyperTwistTrainingRepositoryLibrary::TryGetReviewPlan(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& PlanId,
|
||||
FHyperTwistTrainingReviewPlanState& OutReviewPlan
|
||||
)
|
||||
{
|
||||
OutReviewPlan = FHyperTwistTrainingReviewPlanState();
|
||||
if (PlanId.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingReviewPlanState* ExistingPlan = RepositoryState.ReviewPlans.FindByPredicate(
|
||||
[&PlanId](const FHyperTwistTrainingReviewPlanState& Candidate)
|
||||
{
|
||||
return Candidate.PlanId == PlanId;
|
||||
}
|
||||
);
|
||||
|
||||
if (ExistingPlan == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutReviewPlan = HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(*ExistingPlan);
|
||||
return OutReviewPlan.IsStructurallyValid();
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewPlanState UHyperTwistTrainingRepositoryLibrary::UpdateReviewPlanAfterAttempt(
|
||||
const FHyperTwistTrainingReviewPlanState& ReviewPlan,
|
||||
const FHyperTwistTrainingRunStepResult& StepResult,
|
||||
const FHyperTwistTrainingAttempt& Attempt
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewPlanState UpdatedPlan = ReviewPlan;
|
||||
if (!UpdatedPlan.IsStructurallyValid() || !StepResult.IsStructurallyValid() || !Attempt.IsStructurallyValid())
|
||||
{
|
||||
return UpdatedPlan;
|
||||
}
|
||||
|
||||
const int32 EntryIndex = UpdatedPlan.Entries.IndexOfByPredicate(
|
||||
[&Attempt](const FHyperTwistTrainingReviewPlanEntry& Entry)
|
||||
{
|
||||
return Entry.CaseId == Attempt.CaseId;
|
||||
}
|
||||
);
|
||||
if (EntryIndex < 0)
|
||||
{
|
||||
return UpdatedPlan;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewPlanEntry& Entry = UpdatedPlan.Entries[EntryIndex];
|
||||
Entry.AttemptCount += 1;
|
||||
Entry.LastAttemptId = Attempt.AttemptId;
|
||||
Entry.LastOutcome = Attempt.Result;
|
||||
Entry.LastCompletedAtUtc = Attempt.CompletedAtUtc;
|
||||
|
||||
if (Attempt.Result == EHyperTwistTrainingAttemptResult::Success)
|
||||
{
|
||||
Entry.SuccessCount += 1;
|
||||
Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::Completed;
|
||||
}
|
||||
else if (Attempt.Result == EHyperTwistTrainingAttemptResult::Aborted)
|
||||
{
|
||||
Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::Skipped;
|
||||
}
|
||||
else
|
||||
{
|
||||
Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat;
|
||||
}
|
||||
|
||||
for (int32 Index = 0; Index < UpdatedPlan.Entries.Num(); ++Index)
|
||||
{
|
||||
if (Index == EntryIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (UpdatedPlan.Entries[Index].EntryState == EHyperTwistTrainingReviewPlanEntryState::Current)
|
||||
{
|
||||
UpdatedPlan.Entries[Index].EntryState = EHyperTwistTrainingReviewPlanEntryState::Pending;
|
||||
}
|
||||
}
|
||||
|
||||
const EHyperTwistTrainingSessionState SessionState = StepResult.UpdatedRunState.Session.SessionState;
|
||||
if (SessionState == EHyperTwistTrainingSessionState::Completed
|
||||
|| SessionState == EHyperTwistTrainingSessionState::Aborted)
|
||||
{
|
||||
UpdatedPlan.bCompleted = true;
|
||||
UpdatedPlan.CurrentEntryIndex = INDEX_NONE;
|
||||
UpdatedPlan.CompletedAtUtc = !StepResult.UpdatedRunState.Session.EndedAtUtc.IsEmpty()
|
||||
? StepResult.UpdatedRunState.Session.EndedAtUtc
|
||||
: Attempt.CompletedAtUtc;
|
||||
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(UpdatedPlan);
|
||||
}
|
||||
|
||||
FString NextCaseId;
|
||||
if (StepResult.UpdatedRunState.CurrentSelection.IsStructurallyValid())
|
||||
{
|
||||
NextCaseId = StepResult.UpdatedRunState.CurrentSelection.TrainingCase.CaseId;
|
||||
}
|
||||
if (NextCaseId.IsEmpty())
|
||||
{
|
||||
NextCaseId = StepResult.UpdatedRunState.Session.CurrentCaseId;
|
||||
}
|
||||
|
||||
int32 NextEntryIndex = INDEX_NONE;
|
||||
if (!NextCaseId.IsEmpty())
|
||||
{
|
||||
NextEntryIndex = UpdatedPlan.Entries.IndexOfByPredicate(
|
||||
[&NextCaseId](const FHyperTwistTrainingReviewPlanEntry& Candidate)
|
||||
{
|
||||
return Candidate.CaseId == NextCaseId
|
||||
&& Candidate.EntryState != EHyperTwistTrainingReviewPlanEntryState::Completed
|
||||
&& Candidate.EntryState != EHyperTwistTrainingReviewPlanEntryState::Skipped;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (NextEntryIndex < 0)
|
||||
{
|
||||
NextEntryIndex = UpdatedPlan.Entries.IndexOfByPredicate(
|
||||
[](const FHyperTwistTrainingReviewPlanEntry& Candidate)
|
||||
{
|
||||
return Candidate.EntryState == EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat
|
||||
|| Candidate.EntryState == EHyperTwistTrainingReviewPlanEntryState::Pending;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
UpdatedPlan.CurrentEntryIndex = NextEntryIndex;
|
||||
if (NextEntryIndex >= 0)
|
||||
{
|
||||
UpdatedPlan.Entries[NextEntryIndex].EntryState = EHyperTwistTrainingReviewPlanEntryState::Current;
|
||||
}
|
||||
|
||||
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(UpdatedPlan);
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewFlowStatus UHyperTwistTrainingRepositoryLibrary::DeriveReviewFlowStatus(
|
||||
const FHyperTwistTrainingReviewPlanState& ReviewPlan
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewFlowStatus FlowStatus;
|
||||
if (!ReviewPlan.IsStructurallyValid())
|
||||
{
|
||||
return FlowStatus;
|
||||
}
|
||||
|
||||
FlowStatus.PlanId = ReviewPlan.PlanId;
|
||||
FlowStatus.bPlanComplete = ReviewPlan.bCompleted;
|
||||
|
||||
int32 CurrentEntryIndex = ReviewPlan.CurrentEntryIndex;
|
||||
if (!ReviewPlan.Entries.IsValidIndex(CurrentEntryIndex))
|
||||
{
|
||||
CurrentEntryIndex = ReviewPlan.Entries.IndexOfByPredicate(
|
||||
[](const FHyperTwistTrainingReviewPlanEntry& Entry)
|
||||
{
|
||||
return Entry.EntryState == EHyperTwistTrainingReviewPlanEntryState::Current;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (const FHyperTwistTrainingReviewPlanEntry& Entry : ReviewPlan.Entries)
|
||||
{
|
||||
switch (Entry.EntryState)
|
||||
{
|
||||
case EHyperTwistTrainingReviewPlanEntryState::Pending:
|
||||
FlowStatus.PendingCount += 1;
|
||||
break;
|
||||
case EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat:
|
||||
FlowStatus.RepeatCount += 1;
|
||||
break;
|
||||
case EHyperTwistTrainingReviewPlanEntryState::Completed:
|
||||
FlowStatus.CompletedCount += 1;
|
||||
break;
|
||||
case EHyperTwistTrainingReviewPlanEntryState::Skipped:
|
||||
FlowStatus.SkippedCount += 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (Entry.EntryState == EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat && Entry.AttemptCount >= 2)
|
||||
{
|
||||
FlowStatus.bNeedsCoachIntervention = true;
|
||||
}
|
||||
}
|
||||
|
||||
const bool bCoachFacingRepeatPressure =
|
||||
FlowStatus.RepeatCount > 0
|
||||
&& (
|
||||
ReviewPlan.Policy.PolicyKind == EHyperTwistTrainingReviewPolicyKind::CoachReview
|
||||
|| ReviewPlan.Policy.bPreferRecognitionReplayReview
|
||||
);
|
||||
if (bCoachFacingRepeatPressure)
|
||||
{
|
||||
FlowStatus.bNeedsCoachIntervention = true;
|
||||
}
|
||||
|
||||
const int32 TotalTerminalCount = FlowStatus.CompletedCount + FlowStatus.SkippedCount;
|
||||
if (ReviewPlan.Entries.Num() > 0)
|
||||
{
|
||||
FlowStatus.ProgressRatio = static_cast<float>(TotalTerminalCount) / static_cast<float>(ReviewPlan.Entries.Num());
|
||||
}
|
||||
|
||||
if (ReviewPlan.Entries.IsValidIndex(CurrentEntryIndex))
|
||||
{
|
||||
const FHyperTwistTrainingReviewPlanEntry& CurrentEntry = ReviewPlan.Entries[CurrentEntryIndex];
|
||||
FlowStatus.CurrentCaseId = CurrentEntry.CaseId;
|
||||
FlowStatus.CurrentPromptLabel = CurrentEntry.PromptLabel;
|
||||
FlowStatus.bHasCurrentEntry = true;
|
||||
}
|
||||
|
||||
if (FlowStatus.bPlanComplete || TotalTerminalCount >= ReviewPlan.Entries.Num())
|
||||
{
|
||||
FlowStatus.bPlanComplete = true;
|
||||
FlowStatus.NextActionLabel = TEXT("review-complete");
|
||||
}
|
||||
else if (FlowStatus.bNeedsCoachIntervention)
|
||||
{
|
||||
FlowStatus.NextActionLabel = TEXT("coach-review-needed");
|
||||
}
|
||||
else if (FlowStatus.bHasCurrentEntry)
|
||||
{
|
||||
FlowStatus.NextActionLabel = TEXT("continue-review");
|
||||
}
|
||||
else if (FlowStatus.RepeatCount > 0)
|
||||
{
|
||||
FlowStatus.NextActionLabel = TEXT("repeat-failed-cases");
|
||||
}
|
||||
else if (FlowStatus.PendingCount > 0)
|
||||
{
|
||||
FlowStatus.NextActionLabel = TEXT("advance-to-next-case");
|
||||
}
|
||||
else
|
||||
{
|
||||
FlowStatus.NextActionLabel = TEXT("finalize-review");
|
||||
}
|
||||
|
||||
return FlowStatus;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewProgramSummary UHyperTwistTrainingRepositoryLibrary::DeriveReviewProgramSummary(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& UserId,
|
||||
const FString& ReferenceUtc
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewProgramSummary Summary;
|
||||
if (UserId.IsEmpty())
|
||||
{
|
||||
return Summary;
|
||||
}
|
||||
|
||||
Summary.UserId = UserId;
|
||||
Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc);
|
||||
|
||||
const TArray<FHyperTwistTrainingMethodSegment> MethodSegments = DeriveLearnerMethodSegments(
|
||||
RepositoryState,
|
||||
UserId,
|
||||
Summary.ReferenceUtc
|
||||
);
|
||||
const auto ResolveMethodSegmentIdForDeck = [&MethodSegments](const FString& DeckId) -> FString
|
||||
{
|
||||
for (const FHyperTwistTrainingMethodSegment& MethodSegment : MethodSegments)
|
||||
{
|
||||
if (MethodSegment.DeckIds.Contains(DeckId))
|
||||
{
|
||||
return MethodSegment.MethodDescriptor.SegmentId;
|
||||
}
|
||||
}
|
||||
|
||||
return FString();
|
||||
};
|
||||
|
||||
int32 BestCarryoverScore = -1;
|
||||
FString BestPlanCreatedAtUtc;
|
||||
FString LatestCompletedAtUtc;
|
||||
int32 DeliveryModeCounts[5] = {0, 0, 0, 0, 0};
|
||||
TMap<EHyperTwistTrainingReviewPolicyKind, int32> PolicyCounts;
|
||||
int64 CompletedPlanDurationAccumulator = 0;
|
||||
int32 CompletedPlanDurationCount = 0;
|
||||
float ProgressRatioAccumulator = 0.0f;
|
||||
|
||||
for (const FHyperTwistTrainingReviewPlanState& ReviewPlan : RepositoryState.ReviewPlans)
|
||||
{
|
||||
if (!ReviewPlan.IsStructurallyValid() || ReviewPlan.UserId != UserId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingReviewFlowStatus FlowStatus = DeriveReviewFlowStatus(ReviewPlan);
|
||||
const int32 CarryoverCount = FlowStatus.PendingCount
|
||||
+ FlowStatus.RepeatCount
|
||||
+ (FlowStatus.bHasCurrentEntry ? 1 : 0);
|
||||
const int32 DeliveryModeIndex = FMath::Clamp(static_cast<int32>(ReviewPlan.DeliveryMode), 0, 4);
|
||||
DeliveryModeCounts[DeliveryModeIndex] += 1;
|
||||
PolicyCounts.FindOrAdd(ReviewPlan.Policy.PolicyKind) += 1;
|
||||
Summary.TotalPlanCount += 1;
|
||||
Summary.TotalEntryCount += ReviewPlan.Entries.Num();
|
||||
Summary.CompletedEntryCount += FlowStatus.CompletedCount;
|
||||
Summary.SkippedEntryCount += FlowStatus.SkippedCount;
|
||||
ProgressRatioAccumulator += FlowStatus.ProgressRatio;
|
||||
|
||||
if (Summary.LastPlanCreatedAtUtc.IsEmpty()
|
||||
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
|
||||
ReviewPlan.CreatedAtUtc,
|
||||
Summary.LastPlanCreatedAtUtc
|
||||
) > 0)
|
||||
{
|
||||
Summary.LastPlanCreatedAtUtc = ReviewPlan.CreatedAtUtc;
|
||||
}
|
||||
|
||||
if (ReviewPlan.bCompleted || FlowStatus.bPlanComplete)
|
||||
{
|
||||
Summary.CompletedPlanCount += 1;
|
||||
const FString CompletedAtUtc = !ReviewPlan.CompletedAtUtc.IsEmpty()
|
||||
? ReviewPlan.CompletedAtUtc
|
||||
: ReviewPlan.CreatedAtUtc;
|
||||
if (LatestCompletedAtUtc.IsEmpty()
|
||||
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(CompletedAtUtc, LatestCompletedAtUtc) > 0)
|
||||
{
|
||||
LatestCompletedAtUtc = CompletedAtUtc;
|
||||
Summary.LastCompletedPlanId = ReviewPlan.PlanId;
|
||||
}
|
||||
const int32 PlanDurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputePlanDurationMs(
|
||||
ReviewPlan,
|
||||
Summary.ReferenceUtc
|
||||
);
|
||||
if (PlanDurationMs > 0)
|
||||
{
|
||||
CompletedPlanDurationAccumulator += PlanDurationMs;
|
||||
CompletedPlanDurationCount += 1;
|
||||
if (Summary.BestCompletedPlanDurationMs == 0 || PlanDurationMs < Summary.BestCompletedPlanDurationMs)
|
||||
{
|
||||
Summary.BestCompletedPlanDurationMs = PlanDurationMs;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Summary.PendingPlanIds.Add(ReviewPlan.PlanId);
|
||||
Summary.PendingPlanCount += 1;
|
||||
Summary.PendingEntryCount += FlowStatus.PendingCount;
|
||||
Summary.RepeatEntryCount += FlowStatus.RepeatCount;
|
||||
Summary.CarryoverCaseCount += CarryoverCount;
|
||||
if (FlowStatus.bNeedsCoachIntervention)
|
||||
{
|
||||
Summary.CoachInterventionPlanCount += 1;
|
||||
Summary.bNeedsCoachIntervention = true;
|
||||
}
|
||||
|
||||
const int32 CarryoverScore = CarryoverCount * 100
|
||||
+ (FlowStatus.bNeedsCoachIntervention ? 25 : 0)
|
||||
+ (FlowStatus.bHasCurrentEntry ? 10 : 0);
|
||||
if (CarryoverScore > BestCarryoverScore
|
||||
|| (CarryoverScore == BestCarryoverScore
|
||||
&& (BestPlanCreatedAtUtc.IsEmpty()
|
||||
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(ReviewPlan.CreatedAtUtc, BestPlanCreatedAtUtc) > 0)))
|
||||
{
|
||||
BestCarryoverScore = CarryoverScore;
|
||||
BestPlanCreatedAtUtc = ReviewPlan.CreatedAtUtc;
|
||||
Summary.FocusDeckId = ReviewPlan.SourceDeckId;
|
||||
Summary.FocusMethodSegmentId = ResolveMethodSegmentIdForDeck(ReviewPlan.SourceDeckId);
|
||||
Summary.NextPlanId = ReviewPlan.PlanId;
|
||||
if (FlowStatus.bHasCurrentEntry)
|
||||
{
|
||||
Summary.CurrentPlanId = ReviewPlan.PlanId;
|
||||
}
|
||||
}
|
||||
|
||||
if (Summary.CurrentPlanId.IsEmpty() && FlowStatus.bHasCurrentEntry)
|
||||
{
|
||||
Summary.CurrentPlanId = ReviewPlan.PlanId;
|
||||
}
|
||||
}
|
||||
|
||||
if (Summary.CurrentPlanId.IsEmpty())
|
||||
{
|
||||
Summary.CurrentPlanId = Summary.NextPlanId;
|
||||
}
|
||||
if (Summary.FocusMethodSegmentId.IsEmpty() && !Summary.FocusDeckId.IsEmpty())
|
||||
{
|
||||
Summary.FocusMethodSegmentId = ResolveMethodSegmentIdForDeck(Summary.FocusDeckId);
|
||||
}
|
||||
|
||||
if (Summary.TotalPlanCount > 0)
|
||||
{
|
||||
Summary.AveragePlanProgressRatio = ProgressRatioAccumulator / static_cast<float>(Summary.TotalPlanCount);
|
||||
}
|
||||
if (Summary.PendingPlanCount > 0)
|
||||
{
|
||||
Summary.AveragePendingEntriesPerOpenPlan =
|
||||
static_cast<float>(Summary.PendingEntryCount) / static_cast<float>(Summary.PendingPlanCount);
|
||||
Summary.AverageCarryoverCasesPerOpenPlan =
|
||||
static_cast<float>(Summary.CarryoverCaseCount) / static_cast<float>(Summary.PendingPlanCount);
|
||||
}
|
||||
if (CompletedPlanDurationCount > 0)
|
||||
{
|
||||
Summary.AverageCompletedPlanDurationMs = static_cast<int32>(
|
||||
CompletedPlanDurationAccumulator / CompletedPlanDurationCount
|
||||
);
|
||||
}
|
||||
|
||||
int32 BestModeCount = -1;
|
||||
for (int32 ModeIndex = 0; ModeIndex < UE_ARRAY_COUNT(DeliveryModeCounts); ++ModeIndex)
|
||||
{
|
||||
if (DeliveryModeCounts[ModeIndex] > BestModeCount)
|
||||
{
|
||||
BestModeCount = DeliveryModeCounts[ModeIndex];
|
||||
Summary.DominantDeliveryMode = static_cast<EHyperTwistTrainingDeliveryMode>(ModeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
int32 BestPolicyCount = -1;
|
||||
for (const TPair<EHyperTwistTrainingReviewPolicyKind, int32>& Pair : PolicyCounts)
|
||||
{
|
||||
if (Pair.Value > BestPolicyCount)
|
||||
{
|
||||
BestPolicyCount = Pair.Value;
|
||||
Summary.DominantPolicyKind = Pair.Key;
|
||||
}
|
||||
}
|
||||
|
||||
Summary.ProgramPressureScore =
|
||||
static_cast<float>(Summary.CarryoverCaseCount * 1.0f)
|
||||
+ static_cast<float>(Summary.RepeatEntryCount * 1.5f)
|
||||
+ static_cast<float>(Summary.CoachInterventionPlanCount * 3.0f)
|
||||
+ (Summary.CurrentPlanId.IsEmpty() ? 0.0f : 1.0f);
|
||||
Summary.bHasCarryover = Summary.CarryoverCaseCount > 0 || Summary.PendingPlanCount > 0;
|
||||
return Summary;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingReviewPlanState UHyperTwistTrainingRepositoryLibrary::FinalizeReviewPlan(
|
||||
const FHyperTwistTrainingReviewPlanState& ReviewPlan,
|
||||
const FHyperTwistTrainingSession& Session
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingReviewPlanState UpdatedPlan = ReviewPlan;
|
||||
if (!UpdatedPlan.IsStructurallyValid())
|
||||
{
|
||||
return UpdatedPlan;
|
||||
}
|
||||
|
||||
UpdatedPlan.bCompleted = false;
|
||||
UpdatedPlan.CurrentEntryIndex = INDEX_NONE;
|
||||
UpdatedPlan.CompletedAtUtc = !Session.EndedAtUtc.IsEmpty()
|
||||
? Session.EndedAtUtc
|
||||
: FDateTime::UtcNow().ToIso8601();
|
||||
|
||||
for (FHyperTwistTrainingReviewPlanEntry& Entry : UpdatedPlan.Entries)
|
||||
{
|
||||
if (Entry.EntryState == EHyperTwistTrainingReviewPlanEntryState::Current)
|
||||
{
|
||||
Entry.EntryState = Session.SessionState == EHyperTwistTrainingSessionState::Aborted
|
||||
? EHyperTwistTrainingReviewPlanEntryState::Skipped
|
||||
: EHyperTwistTrainingReviewPlanEntryState::Pending;
|
||||
}
|
||||
}
|
||||
|
||||
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(UpdatedPlan);
|
||||
}
|
||||
|
|
@ -1588,3 +1588,48 @@ Latest exact-source public/manual parity follow-up later on `2026-06-29`:
|
|||
hotspot exists, so the next native refactor seam remains a real task
|
||||
- the right next refactor target remains the same-family training-repository
|
||||
extraction work rather than reopening browser or product topology
|
||||
|
||||
Latest native repository review-family split follow-up later on `2026-06-29`:
|
||||
|
||||
- the next same-family native continuation reduced the largest remaining
|
||||
training-repository hotspot without widening product scope:
|
||||
- moved the review-policy, recommendation shaping, review-deck, review-plan,
|
||||
and review-program public method family out of
|
||||
`HyperTwistTrainingRepositoryLibrary.cpp`
|
||||
- added a private internal declaration header at
|
||||
`UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h`
|
||||
so the split translation unit can share the bounded internal helpers it
|
||||
actually needs
|
||||
- added the dedicated implementation unit at
|
||||
`UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryReview.cpp`
|
||||
- reduced the main repository file to `31,976` lines while the extracted
|
||||
review-family unit now holds `809` lines
|
||||
- the HyperTwist-owned structural loop stayed flat on that exact native split:
|
||||
- `scripts/run-hypertwist-sentrux-source-only.sh`
|
||||
- `Quality: 6234`
|
||||
- all `7` rules pass
|
||||
- `scripts/run-hypertwist-sentrux-gate.sh`
|
||||
- comparison result:
|
||||
- `Quality: 6234 -> 6234`
|
||||
- `Coupling: 0.15 -> 0.15`
|
||||
- `Cycles: 0 -> 0`
|
||||
- `God files: 1 -> 1`
|
||||
- `No degradation detected`
|
||||
- the canonical Windows Unreal proof for that native packet was then recovered
|
||||
through the maintained reverse-SSH lane:
|
||||
- the earlier long full rebuild compiled the touched split files plus linked
|
||||
the main `UnrealHyperTwist` and `UnrealMCPChong` editor modules cleanly,
|
||||
but its final session handle was lost before a trustworthy close banner was
|
||||
captured
|
||||
- the packet was therefore revalidated honestly instead of inferred:
|
||||
- reran `scripts/run-hypertwist-remote-unreal-build.sh --worktree-root 'C:\htpp'`
|
||||
- `Result: Succeeded`
|
||||
- UnrealBuildTool `Total execution time: 4.08 seconds`
|
||||
- current truthful reading after this native follow-up:
|
||||
- the extracted review-family seam is now build-proven on the canonical
|
||||
Windows lane and safe to land as a real native maintainability packet
|
||||
- the remaining native refactor work is no longer whether this split is
|
||||
valid; it is the next same-family hotspot extraction inside the training
|
||||
repository library
|
||||
- the best next bounded seam remains the coach action-plan and
|
||||
coach-session-queue cluster rather than any fresh product-family widening
|
||||
|
|
|
|||
|
|
@ -2414,3 +2414,45 @@ Latest same-family responsive public-route expansion follow-up on `2026-06-29`:
|
|||
remains a preview deployment rather than full public launch because the
|
||||
checkout, Windows download, billing-secret, and non-loopback auth-core
|
||||
launch conditions are not yet satisfied
|
||||
|
||||
## Latest native repository review-family split follow-up later on `2026-06-29`
|
||||
|
||||
- the next same-family native continuation then reduced the largest remaining
|
||||
training-repository hotspot without widening simulator or website scope:
|
||||
- the review-policy, recommendation shaping, review-deck, review-plan, and
|
||||
review-program public method family was moved out of
|
||||
`HyperTwistTrainingRepositoryLibrary.cpp`
|
||||
- a private helper declaration seam now lives at
|
||||
`UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryInternal.h`
|
||||
- the extracted implementation family now lives at
|
||||
`UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibraryReview.cpp`
|
||||
- the main repository file now stands at `31,976` lines and the extracted
|
||||
review-family unit stands at `809` lines
|
||||
- the HyperTwist-owned structural loop stayed healthy on that exact native
|
||||
state:
|
||||
- `scripts/run-hypertwist-sentrux-source-only.sh`
|
||||
- `Quality: 6234`
|
||||
- all `7` rules pass
|
||||
- `scripts/run-hypertwist-sentrux-gate.sh`
|
||||
- comparison result:
|
||||
- `Quality: 6234 -> 6234`
|
||||
- `Coupling: 0.15 -> 0.15`
|
||||
- `Cycles: 0 -> 0`
|
||||
- `God files: 1 -> 1`
|
||||
- `No degradation detected`
|
||||
- the canonical Windows Unreal proof for that native split was then recovered
|
||||
honestly through the maintained reverse-SSH lane:
|
||||
- the first long full rebuild had already compiled the touched split files
|
||||
and linked the main editor modules cleanly, but the final session handle
|
||||
was lost before a trustworthy terminal banner could be captured
|
||||
- the packet was therefore rerun rather than inferred:
|
||||
- `scripts/run-hypertwist-remote-unreal-build.sh --worktree-root 'C:\htpp'`
|
||||
- `Result: Succeeded`
|
||||
- UnrealBuildTool `Total execution time: 4.08 seconds`
|
||||
- current truthful reading after this native follow-up:
|
||||
- the review-family extraction is now build-proven on the canonical Windows
|
||||
lane
|
||||
- vanilla refactoring still is not fully complete because the training
|
||||
repository library still contains the remaining same-family hotspot
|
||||
- the best next bounded native seam remains the coach action-plan and
|
||||
coach-session-queue cluster instead of any fresh product-family widening
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue