Add template launch scoped analytics
This commit is contained in:
parent
c17bfca008
commit
2d20d0ba60
6 changed files with 603 additions and 14 deletions
|
|
@ -83,6 +83,56 @@ namespace HyperTwistCoachDashboardWidgetInternal
|
|||
return Entry;
|
||||
}
|
||||
|
||||
FString BuildTemplateLaunchAnalyticsOutcomeFragment(
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats
|
||||
)
|
||||
{
|
||||
if (TemplateStats == nullptr || !TemplateStats->IsStructurallyValid())
|
||||
{
|
||||
return TEXT("no stored template-backed outcomes yet");
|
||||
}
|
||||
|
||||
return FString::Printf(
|
||||
TEXT("launches %d | completed %d | aborted %d | open %d | success %.2f"),
|
||||
TemplateStats->LaunchCount,
|
||||
TemplateStats->CompletedRunCount,
|
||||
TemplateStats->AbortedRunCount,
|
||||
TemplateStats->OpenRunCount,
|
||||
TemplateStats->OverallSuccessRate
|
||||
);
|
||||
}
|
||||
|
||||
FString BuildTemplateLaunchAnalyticsDetailFragment(
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* TemplateStats
|
||||
)
|
||||
{
|
||||
if (TemplateStats == nullptr || !TemplateStats->IsStructurallyValid())
|
||||
{
|
||||
return TEXT("no retained per-template analytics are available for this template id yet");
|
||||
}
|
||||
|
||||
return FString::Printf(
|
||||
TEXT("avg recommended %d | avg focus %d | avg completed attempts %d | total success/failure/timeout/dnf %d/%d/%d/%d | avg mistakes %.2f | review-backed %d | coach-review %d | short set %d | last launch %s | last completed %s"),
|
||||
TemplateStats->AverageRecommendedCaseCount,
|
||||
TemplateStats->AverageFocusCaseCount,
|
||||
TemplateStats->AverageCompletedAttemptCount,
|
||||
TemplateStats->TotalSuccessCount,
|
||||
TemplateStats->TotalFailureCount,
|
||||
TemplateStats->TotalTimeoutCount,
|
||||
TemplateStats->TotalDNFCount,
|
||||
TemplateStats->AverageMistakesPerOutcomeRun,
|
||||
TemplateStats->ReviewBackedLaunchCount,
|
||||
TemplateStats->CoachReviewLaunchCount,
|
||||
TemplateStats->ShortReviewSetLaunchCount,
|
||||
TemplateStats->LastLaunchAtUtc.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: *TemplateStats->LastLaunchAtUtc,
|
||||
TemplateStats->LastCompletedAtUtc.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: *TemplateStats->LastCompletedAtUtc
|
||||
);
|
||||
}
|
||||
|
||||
struct FClosureRecoveryDashboardBias
|
||||
{
|
||||
int32 ContinuationBias = 0;
|
||||
|
|
@ -6992,6 +7042,34 @@ const FHyperTwistTrainingSessionTemplate* UHyperTwistCoachDashboardWidget::FindP
|
|||
return PreferredScore > 0 ? PreferredTemplate : nullptr;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats*
|
||||
UHyperTwistCoachDashboardWidget::FindCachedTemplateLaunchScopedStats(
|
||||
const FString& TemplateId,
|
||||
const FString& UserId
|
||||
) const
|
||||
{
|
||||
if (TemplateId.IsEmpty()
|
||||
|| CachedTemplateLaunchScopedStats.Num() <= 0)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!UserId.IsEmpty()
|
||||
&& !CachedTemplateLaunchAnalyticsUserId.IsEmpty()
|
||||
&& UserId != CachedTemplateLaunchAnalyticsUserId)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return CachedTemplateLaunchScopedStats.FindByPredicate(
|
||||
[&TemplateId, &UserId](const FHyperTwistTrainingTemplateLaunchScopedStats& Candidate)
|
||||
{
|
||||
return Candidate.TemplateId == TemplateId
|
||||
&& (UserId.IsEmpty() || Candidate.UserId.IsEmpty() || Candidate.UserId == UserId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void UHyperTwistCoachDashboardWidget::CaptureActiveGuidanceLaunchContext(
|
||||
const FString& SessionId,
|
||||
const FHyperTwistTrainingDeck& Deck,
|
||||
|
|
@ -9945,6 +10023,8 @@ void UHyperTwistCoachDashboardWidget::SyncTemplateLaunchHistoryFromRepository()
|
|||
? TemplateLaunchHistoryEntries[SelectedTemplateLaunchHistoryIndex].SourceSessionId
|
||||
: FString();
|
||||
|
||||
CachedTemplateLaunchAnalyticsUserId.Reset();
|
||||
CachedTemplateLaunchScopedStats.Reset();
|
||||
TemplateLaunchHistoryEntries.Reset();
|
||||
SelectedTemplateLaunchHistoryIndex = INDEX_NONE;
|
||||
|
||||
|
|
@ -9978,6 +10058,12 @@ void UHyperTwistCoachDashboardWidget::SyncTemplateLaunchHistoryFromRepository()
|
|||
|
||||
const FHyperTwistTrainingRepositoryState RepositoryState =
|
||||
UHyperTwistTrainingRuntimeLibrary::GetTrainingRepositoryState(this);
|
||||
CachedTemplateLaunchAnalyticsUserId = HistoryUserId;
|
||||
CachedTemplateLaunchScopedStats =
|
||||
UHyperTwistTrainingRepositoryLibrary::ListTemplateLaunchScopedStatsForUser(
|
||||
RepositoryState,
|
||||
HistoryUserId,
|
||||
FString());
|
||||
const TArray<FHyperTwistTrainingRunRecord> UserRunRecords =
|
||||
UHyperTwistTrainingRepositoryLibrary::ListRunRecordsForDeck(
|
||||
RepositoryState,
|
||||
|
|
@ -11211,6 +11297,17 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
|
|||
bHasActiveTemplateLaunchForDisplayedRun
|
||||
? &ActiveTemplateLaunchProvenance
|
||||
: (bHasDisplayedRetainedTemplateLaunch ? &RetainedTemplateLaunchProvenance : nullptr);
|
||||
const FString DisplayedTemplateLaunchUserId = DisplayedTemplateLaunch != nullptr
|
||||
? (DisplayedTemplateLaunch->UserId.IsEmpty()
|
||||
? (bHasCurrentRunRecap ? CachedRunState.Session.UserId : RetainedRunRecapUserId)
|
||||
: DisplayedTemplateLaunch->UserId)
|
||||
: FString();
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* DisplayedTemplateLaunchStats =
|
||||
DisplayedTemplateLaunch != nullptr
|
||||
? FindCachedTemplateLaunchScopedStats(
|
||||
DisplayedTemplateLaunch->TemplateId,
|
||||
DisplayedTemplateLaunchUserId)
|
||||
: nullptr;
|
||||
const bool bDisplayedTemplateLaunchUsesReviewLane =
|
||||
DisplayedTemplateLaunch != nullptr
|
||||
&& (DisplayedTemplateLaunch->bPreferReviewRun
|
||||
|
|
@ -11273,9 +11370,18 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
|
|||
? PreferredTemplateLaunch->TemplateId
|
||||
: PreferredTemplateLaunch->Title)
|
||||
: TEXT("n/a");
|
||||
const FString PreferredTemplateLaunchUserId = bHasPreferredTemplateLaunch
|
||||
? (PreferredTemplateLaunch->UserId.IsEmpty() ? DefaultUserId : PreferredTemplateLaunch->UserId)
|
||||
: FString();
|
||||
const FString PreferredTemplateLaunchFocusPreview = bHasPreferredTemplateLaunch
|
||||
? BuildCaseIdPreview(PreferredTemplateLaunch->FocusCaseIds)
|
||||
: TEXT("none");
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* PreferredTemplateLaunchStats =
|
||||
bHasPreferredTemplateLaunch
|
||||
? FindCachedTemplateLaunchScopedStats(
|
||||
PreferredTemplateLaunch->TemplateId,
|
||||
PreferredTemplateLaunchUserId)
|
||||
: nullptr;
|
||||
const bool bCanLaunchPreferredTemplate =
|
||||
!bHasLiveAttemptTimer && !bHasActiveRun && bHasPreferredTemplateLaunch;
|
||||
FString TemplateLaunchStatusLine;
|
||||
|
|
@ -11290,27 +11396,33 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
|
|||
else if (bHasLiveAttemptTimer)
|
||||
{
|
||||
TemplateLaunchStatusLine = FString::Printf(
|
||||
TEXT("Template launch: %s is ready, but a live attempt is active."),
|
||||
*PreferredTemplateLaunchTitle
|
||||
TEXT("Template launch: %s is ready, but a live attempt is active. Past outcomes: %s."),
|
||||
*PreferredTemplateLaunchTitle,
|
||||
*HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsOutcomeFragment(
|
||||
PreferredTemplateLaunchStats)
|
||||
);
|
||||
}
|
||||
else if (bHasActiveRun)
|
||||
{
|
||||
TemplateLaunchStatusLine = FString::Printf(
|
||||
TEXT("Template launch: %s is ready, but clear or complete the active run first."),
|
||||
*PreferredTemplateLaunchTitle
|
||||
TEXT("Template launch: %s is ready, but clear or complete the active run first. Past outcomes: %s."),
|
||||
*PreferredTemplateLaunchTitle,
|
||||
*HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsOutcomeFragment(
|
||||
PreferredTemplateLaunchStats)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
TemplateLaunchStatusLine = FString::Printf(
|
||||
TEXT("Template launch: %s is ready on %s. Eligible %d of %d active templates."),
|
||||
TEXT("Template launch: %s is ready on %s. Eligible %d of %d active templates. Past outcomes: %s."),
|
||||
*PreferredTemplateLaunchTitle,
|
||||
PreferredTemplateLaunch->FocusDeckId.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: *PreferredTemplateLaunch->FocusDeckId,
|
||||
EligibleTemplateLaunchCount,
|
||||
CachedActiveTrainingSessionTemplates.Num()
|
||||
CachedActiveTrainingSessionTemplates.Num(),
|
||||
*HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsOutcomeFragment(
|
||||
PreferredTemplateLaunchStats)
|
||||
);
|
||||
}
|
||||
FString TemplateLaunchDetailLine;
|
||||
|
|
@ -11322,7 +11434,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
|
|||
else
|
||||
{
|
||||
TemplateLaunchDetailLine = FString::Printf(
|
||||
TEXT("Template: %s | id: %s | kind: %s | mode: %s | selection: %s | recommended %d | focus cases: %s | guidance: %s | method: %s | user: %s | source plan: %s | review-backed: %s | short set: %s"),
|
||||
TEXT("Template: %s | id: %s | kind: %s | mode: %s | selection: %s | recommended %d | focus cases: %s | guidance: %s | method: %s | user: %s | source plan: %s | review-backed: %s | short set: %s | Past detail: %s"),
|
||||
*PreferredTemplateLaunchTitle,
|
||||
PreferredTemplateLaunch->TemplateId.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
|
|
@ -11348,7 +11460,9 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
|
|||
? TEXT("none")
|
||||
: *PreferredTemplateLaunch->SourceReviewPlanId,
|
||||
bPreferredTemplateLaunchUsesReviewLane ? TEXT("yes") : TEXT("no"),
|
||||
PreferredTemplateLaunch->bPreferShortReviewSet ? TEXT("yes") : TEXT("no")
|
||||
PreferredTemplateLaunch->bPreferShortReviewSet ? TEXT("yes") : TEXT("no"),
|
||||
*HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsDetailFragment(
|
||||
PreferredTemplateLaunchStats)
|
||||
);
|
||||
}
|
||||
const FHyperTwistCoachDashboardTemplateLaunchHistoryEntry* SelectedTemplateLaunchHistoryEntry =
|
||||
|
|
@ -11356,10 +11470,16 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
|
|||
&& SelectedTemplateLaunchHistoryIndex < TemplateLaunchHistoryEntries.Num()
|
||||
? &TemplateLaunchHistoryEntries[SelectedTemplateLaunchHistoryIndex]
|
||||
: nullptr;
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* SelectedTemplateLaunchHistoryStats =
|
||||
SelectedTemplateLaunchHistoryEntry != nullptr
|
||||
? FindCachedTemplateLaunchScopedStats(
|
||||
SelectedTemplateLaunchHistoryEntry->TemplateId,
|
||||
SelectedTemplateLaunchHistoryEntry->UserId)
|
||||
: nullptr;
|
||||
const FString TemplateLaunchHistoryStatusLine =
|
||||
SelectedTemplateLaunchHistoryEntry != nullptr
|
||||
? FString::Printf(
|
||||
TEXT("Template launch history: %d entries | selected %d/%d | Template: %s | State: %s | Completed: %s"),
|
||||
TEXT("Template launch history: %d entries | selected %d/%d | Template: %s | State: %s | Completed: %s | Aggregate: %s"),
|
||||
TemplateLaunchHistoryEntries.Num(),
|
||||
SelectedTemplateLaunchHistoryIndex + 1,
|
||||
TemplateLaunchHistoryEntries.Num(),
|
||||
|
|
@ -11370,12 +11490,14 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
|
|||
SelectedTemplateLaunchHistoryEntry->SessionState),
|
||||
SelectedTemplateLaunchHistoryEntry->CompletedAtUtc.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: *SelectedTemplateLaunchHistoryEntry->CompletedAtUtc)
|
||||
: *SelectedTemplateLaunchHistoryEntry->CompletedAtUtc,
|
||||
*HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsOutcomeFragment(
|
||||
SelectedTemplateLaunchHistoryStats))
|
||||
: TEXT("Template launch history: no completed template-backed runs retained yet.");
|
||||
const FString TemplateLaunchHistoryDetailLine =
|
||||
SelectedTemplateLaunchHistoryEntry != nullptr
|
||||
? FString::Printf(
|
||||
TEXT("Title: %s | kind: %s | mode: %s | selection: %s | deck: %s | user: %s | source plan: %s | recommended %d | focus %d | attempts %d | success %.2f | mistakes %d | review-backed: %s | coach-review: %s | short set: %s"),
|
||||
TEXT("Title: %s | kind: %s | mode: %s | selection: %s | deck: %s | user: %s | source plan: %s | recommended %d | focus %d | attempts %d | success %.2f | mistakes %d | review-backed: %s | coach-review: %s | short set: %s | Aggregate detail: %s"),
|
||||
SelectedTemplateLaunchHistoryEntry->TemplateTitle.IsEmpty()
|
||||
? TEXT("n/a")
|
||||
: *SelectedTemplateLaunchHistoryEntry->TemplateTitle,
|
||||
|
|
@ -11401,7 +11523,9 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
|
|||
SelectedTemplateLaunchHistoryEntry->TotalMistakes,
|
||||
SelectedTemplateLaunchHistoryEntry->bPreferReviewRun ? TEXT("yes") : TEXT("no"),
|
||||
SelectedTemplateLaunchHistoryEntry->bCoachReview ? TEXT("yes") : TEXT("no"),
|
||||
SelectedTemplateLaunchHistoryEntry->bPreferShortReviewSet ? TEXT("yes") : TEXT("no"))
|
||||
SelectedTemplateLaunchHistoryEntry->bPreferShortReviewSet ? TEXT("yes") : TEXT("no"),
|
||||
*HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsDetailFragment(
|
||||
SelectedTemplateLaunchHistoryStats))
|
||||
: TEXT("Template launch history detail: completed runs started through the dashboard template lane will accumulate here with template id, template kind, and source review-plan provenance.");
|
||||
const bool bCanUseCoachHandoff = !bHasLiveAttemptTimer && !bHasActiveRun;
|
||||
FString CoachHandoffStatusLine;
|
||||
|
|
@ -15416,7 +15540,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
|
|||
RunRecapDetailTextBlock->SetText(FText::FromString(
|
||||
DisplayedTemplateLaunch != nullptr
|
||||
? FString::Printf(
|
||||
TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s | Template title %s | Template mode %s | Template selection %s | Recommended %d | Focus %d | Review-backed %s"),
|
||||
TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s | Template title %s | Template mode %s | Template selection %s | Recommended %d | Focus %d | Review-backed %s | Template aggregate %s | Template detail %s"),
|
||||
DisplayedRunRecap->CompletedAttemptCount,
|
||||
DisplayedRunRecap->AttemptCount,
|
||||
DisplayedRunRecap->SuccessCount,
|
||||
|
|
@ -15440,7 +15564,11 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
|
|||
DisplayedTemplateLaunch->SuggestedSelectionPolicy),
|
||||
DisplayedTemplateLaunch->RecommendedCaseCount,
|
||||
DisplayedTemplateLaunch->FocusCaseIds.Num(),
|
||||
bDisplayedTemplateLaunchUsesReviewLane ? TEXT("yes") : TEXT("no"))
|
||||
bDisplayedTemplateLaunchUsesReviewLane ? TEXT("yes") : TEXT("no"),
|
||||
*HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsOutcomeFragment(
|
||||
DisplayedTemplateLaunchStats),
|
||||
*HyperTwistCoachDashboardWidgetInternal::BuildTemplateLaunchAnalyticsDetailFragment(
|
||||
DisplayedTemplateLaunchStats))
|
||||
: FString::Printf(
|
||||
TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s"),
|
||||
DisplayedRunRecap->CompletedAttemptCount,
|
||||
|
|
|
|||
|
|
@ -9102,6 +9102,235 @@ FHyperTwistTrainingDeckScopedStats UHyperTwistTrainingRepositoryLibrary::DeriveD
|
|||
return Stats;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingTemplateLaunchScopedStats UHyperTwistTrainingRepositoryLibrary::DeriveTemplateLaunchScopedStats(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& UserId,
|
||||
const FString& TemplateId,
|
||||
const FString& ReferenceUtc
|
||||
)
|
||||
{
|
||||
FHyperTwistTrainingTemplateLaunchScopedStats ScopedStats;
|
||||
if (UserId.IsEmpty() || TemplateId.IsEmpty())
|
||||
{
|
||||
return ScopedStats;
|
||||
}
|
||||
|
||||
const TArray<FHyperTwistTrainingTemplateLaunchScopedStats> TemplateScopedStats =
|
||||
ListTemplateLaunchScopedStatsForUser(
|
||||
RepositoryState,
|
||||
UserId,
|
||||
ReferenceUtc
|
||||
);
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* ExistingStats =
|
||||
TemplateScopedStats.FindByPredicate(
|
||||
[&TemplateId](const FHyperTwistTrainingTemplateLaunchScopedStats& Candidate)
|
||||
{
|
||||
return Candidate.TemplateId == TemplateId;
|
||||
}
|
||||
);
|
||||
return ExistingStats != nullptr ? *ExistingStats : ScopedStats;
|
||||
}
|
||||
|
||||
TArray<FHyperTwistTrainingTemplateLaunchScopedStats>
|
||||
UHyperTwistTrainingRepositoryLibrary::ListTemplateLaunchScopedStatsForUser(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& UserId,
|
||||
const FString& ReferenceUtc
|
||||
)
|
||||
{
|
||||
TArray<FHyperTwistTrainingTemplateLaunchScopedStats> ScopedStats;
|
||||
if (UserId.IsEmpty())
|
||||
{
|
||||
return ScopedStats;
|
||||
}
|
||||
|
||||
struct FTemplateLaunchScopedStatsAccumulator
|
||||
{
|
||||
FHyperTwistTrainingTemplateLaunchScopedStats Stats;
|
||||
int64 RecommendedCaseAccumulator = 0;
|
||||
int64 FocusCaseAccumulator = 0;
|
||||
int64 CompletedAttemptAccumulator = 0;
|
||||
int64 MistakeAccumulator = 0;
|
||||
int32 OutcomeRunSampleCount = 0;
|
||||
};
|
||||
|
||||
const FString ResolvedReferenceUtc =
|
||||
HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc);
|
||||
const TArray<FHyperTwistTrainingRunRecord> RunRecords =
|
||||
ListRunRecordsForDeck(RepositoryState, UserId, FString());
|
||||
TArray<FTemplateLaunchScopedStatsAccumulator> Accumulators;
|
||||
TMap<FString, int32> AccumulatorIndexByTemplateId;
|
||||
for (const FHyperTwistTrainingRunRecord& RunRecord : RunRecords)
|
||||
{
|
||||
if (!RunRecord.IsStructurallyValid()
|
||||
|| !RunRecord.TemplateLaunchProvenance.IsStructurallyValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingTemplateLaunchProvenance& Provenance =
|
||||
RunRecord.TemplateLaunchProvenance;
|
||||
const FString ScopedUserId = !Provenance.UserId.IsEmpty()
|
||||
? Provenance.UserId
|
||||
: RunRecord.Session.UserId;
|
||||
if (ScopedUserId != UserId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int32* ExistingIndex = AccumulatorIndexByTemplateId.Find(Provenance.TemplateId);
|
||||
if (ExistingIndex == nullptr)
|
||||
{
|
||||
FTemplateLaunchScopedStatsAccumulator Accumulator;
|
||||
Accumulator.Stats.UserId = UserId;
|
||||
Accumulator.Stats.TemplateId = Provenance.TemplateId;
|
||||
Accumulator.Stats.ReferenceUtc = ResolvedReferenceUtc;
|
||||
Accumulator.Stats.SourceTemplateId = Provenance.SourceTemplateId;
|
||||
Accumulator.Stats.Title = Provenance.Title;
|
||||
Accumulator.Stats.FocusDeckId = Provenance.FocusDeckId;
|
||||
Accumulator.Stats.SourceReviewPlanId = Provenance.SourceReviewPlanId;
|
||||
Accumulator.Stats.TemplateKind = Provenance.TemplateKind;
|
||||
Accumulator.Stats.SuggestedMode = Provenance.SuggestedMode;
|
||||
Accumulator.Stats.SuggestedSelectionPolicy = Provenance.SuggestedSelectionPolicy;
|
||||
const int32 AddedIndex = Accumulators.Add(MoveTemp(Accumulator));
|
||||
AccumulatorIndexByTemplateId.Add(Provenance.TemplateId, AddedIndex);
|
||||
ExistingIndex = AccumulatorIndexByTemplateId.Find(Provenance.TemplateId);
|
||||
}
|
||||
|
||||
FTemplateLaunchScopedStatsAccumulator& Accumulator = Accumulators[*ExistingIndex];
|
||||
const FString LaunchAtUtc = !RunRecord.Session.StartedAtUtc.IsEmpty()
|
||||
? RunRecord.Session.StartedAtUtc
|
||||
: RunRecord.LastUpdatedAtUtc;
|
||||
if (Accumulator.Stats.LastLaunchAtUtc.IsEmpty()
|
||||
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
|
||||
LaunchAtUtc,
|
||||
Accumulator.Stats.LastLaunchAtUtc) > 0)
|
||||
{
|
||||
Accumulator.Stats.SourceTemplateId = Provenance.SourceTemplateId;
|
||||
Accumulator.Stats.Title = Provenance.Title;
|
||||
Accumulator.Stats.FocusDeckId = Provenance.FocusDeckId;
|
||||
Accumulator.Stats.SourceReviewPlanId = Provenance.SourceReviewPlanId;
|
||||
Accumulator.Stats.TemplateKind = Provenance.TemplateKind;
|
||||
Accumulator.Stats.SuggestedMode = Provenance.SuggestedMode;
|
||||
Accumulator.Stats.SuggestedSelectionPolicy = Provenance.SuggestedSelectionPolicy;
|
||||
Accumulator.Stats.LastLaunchAtUtc = LaunchAtUtc;
|
||||
Accumulator.Stats.LastSessionId = RunRecord.Session.TrainingSessionId;
|
||||
}
|
||||
|
||||
Accumulator.Stats.LaunchCount += 1;
|
||||
Accumulator.Stats.TotalAttemptCount += RunRecord.Summary.AttemptCount;
|
||||
Accumulator.Stats.TotalCompletedAttemptCount += RunRecord.Summary.CompletedAttemptCount;
|
||||
Accumulator.Stats.TotalSuccessCount += RunRecord.Summary.SuccessCount;
|
||||
Accumulator.Stats.TotalFailureCount += RunRecord.Summary.FailureCount;
|
||||
Accumulator.Stats.TotalTimeoutCount += RunRecord.Summary.TimeoutCount;
|
||||
Accumulator.Stats.TotalDNFCount += RunRecord.Summary.DNFCount;
|
||||
Accumulator.Stats.TotalMistakes += RunRecord.Summary.TotalMistakes;
|
||||
Accumulator.RecommendedCaseAccumulator += FMath::Max(Provenance.RecommendedCaseCount, 0);
|
||||
Accumulator.FocusCaseAccumulator += Provenance.FocusCaseIds.Num();
|
||||
|
||||
const bool bReviewBackedLaunch =
|
||||
Provenance.bPreferReviewRun
|
||||
|| Provenance.TemplateKind == EHyperTwistTrainingSessionTemplateKind::ReviewPlan
|
||||
|| Provenance.TemplateKind == EHyperTwistTrainingSessionTemplateKind::Coach;
|
||||
if (bReviewBackedLaunch)
|
||||
{
|
||||
Accumulator.Stats.ReviewBackedLaunchCount += 1;
|
||||
}
|
||||
if (Provenance.bCoachReview)
|
||||
{
|
||||
Accumulator.Stats.CoachReviewLaunchCount += 1;
|
||||
}
|
||||
if (Provenance.bPreferShortReviewSet)
|
||||
{
|
||||
Accumulator.Stats.ShortReviewSetLaunchCount += 1;
|
||||
}
|
||||
|
||||
if (RunRecord.Session.SessionState == EHyperTwistTrainingSessionState::Completed)
|
||||
{
|
||||
Accumulator.Stats.CompletedRunCount += 1;
|
||||
}
|
||||
else if (RunRecord.Session.SessionState == EHyperTwistTrainingSessionState::Aborted)
|
||||
{
|
||||
Accumulator.Stats.AbortedRunCount += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Accumulator.Stats.OpenRunCount += 1;
|
||||
}
|
||||
|
||||
if (RunRecord.Session.SessionState == EHyperTwistTrainingSessionState::Completed
|
||||
|| RunRecord.Session.SessionState == EHyperTwistTrainingSessionState::Aborted)
|
||||
{
|
||||
const FString CompletedAtUtc = !RunRecord.Summary.LastCompletedAtUtc.IsEmpty()
|
||||
? RunRecord.Summary.LastCompletedAtUtc
|
||||
: RunRecord.Session.EndedAtUtc;
|
||||
if (!CompletedAtUtc.IsEmpty()
|
||||
&& (Accumulator.Stats.LastCompletedAtUtc.IsEmpty()
|
||||
|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
|
||||
CompletedAtUtc,
|
||||
Accumulator.Stats.LastCompletedAtUtc) > 0))
|
||||
{
|
||||
Accumulator.Stats.LastCompletedAtUtc = CompletedAtUtc;
|
||||
}
|
||||
|
||||
Accumulator.CompletedAttemptAccumulator += RunRecord.Summary.CompletedAttemptCount;
|
||||
Accumulator.MistakeAccumulator += RunRecord.Summary.TotalMistakes;
|
||||
Accumulator.OutcomeRunSampleCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (FTemplateLaunchScopedStatsAccumulator& Accumulator : Accumulators)
|
||||
{
|
||||
Accumulator.Stats.AverageRecommendedCaseCount = Accumulator.Stats.LaunchCount > 0
|
||||
? static_cast<int32>(
|
||||
Accumulator.RecommendedCaseAccumulator / Accumulator.Stats.LaunchCount)
|
||||
: 0;
|
||||
Accumulator.Stats.AverageFocusCaseCount = Accumulator.Stats.LaunchCount > 0
|
||||
? static_cast<int32>(
|
||||
Accumulator.FocusCaseAccumulator / Accumulator.Stats.LaunchCount)
|
||||
: 0;
|
||||
Accumulator.Stats.AverageCompletedAttemptCount = Accumulator.OutcomeRunSampleCount > 0
|
||||
? static_cast<int32>(
|
||||
Accumulator.CompletedAttemptAccumulator / Accumulator.OutcomeRunSampleCount)
|
||||
: 0;
|
||||
Accumulator.Stats.OverallSuccessRate = Accumulator.Stats.TotalCompletedAttemptCount > 0
|
||||
? static_cast<float>(Accumulator.Stats.TotalSuccessCount)
|
||||
/ static_cast<float>(Accumulator.Stats.TotalCompletedAttemptCount)
|
||||
: 0.0f;
|
||||
Accumulator.Stats.AverageMistakesPerOutcomeRun = Accumulator.OutcomeRunSampleCount > 0
|
||||
? static_cast<float>(Accumulator.MistakeAccumulator)
|
||||
/ static_cast<float>(Accumulator.OutcomeRunSampleCount)
|
||||
: 0.0f;
|
||||
if (Accumulator.Stats.IsStructurallyValid())
|
||||
{
|
||||
ScopedStats.Add(Accumulator.Stats);
|
||||
}
|
||||
}
|
||||
|
||||
ScopedStats.Sort([](
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats& Left,
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats& Right
|
||||
)
|
||||
{
|
||||
const int32 LaunchAtCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
|
||||
Left.LastLaunchAtUtc,
|
||||
Right.LastLaunchAtUtc
|
||||
);
|
||||
if (LaunchAtCompare != 0)
|
||||
{
|
||||
return LaunchAtCompare > 0;
|
||||
}
|
||||
if (Left.LaunchCount != Right.LaunchCount)
|
||||
{
|
||||
return Left.LaunchCount > Right.LaunchCount;
|
||||
}
|
||||
|
||||
return Left.TemplateId < Right.TemplateId;
|
||||
});
|
||||
|
||||
return ScopedStats;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingLearnerDeckStateSummary UHyperTwistTrainingRepositoryLibrary::DeriveLearnerDeckStateSummary(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& UserId,
|
||||
|
|
|
|||
|
|
@ -1583,6 +1583,10 @@ private:
|
|||
const FHyperTwistTrainingTimingPolicy* FindActiveTimingPolicy() const;
|
||||
const FHyperTwistTrainingDeck* GetActiveGuidancePreviewDeck() const;
|
||||
const FHyperTwistCoachBrief* GetActiveGuidancePreviewBrief() const;
|
||||
const FHyperTwistTrainingTemplateLaunchScopedStats* FindCachedTemplateLaunchScopedStats(
|
||||
const FString& TemplateId,
|
||||
const FString& UserId
|
||||
) const;
|
||||
const FHyperTwistTrainingSessionTemplate* FindPreferredDashboardTemplateLaunch() const;
|
||||
FString ResolveDashboardHandoffStartActionSourceLabel(
|
||||
EHyperTwistTrainingCoachHandoffKind HandoffKind) const;
|
||||
|
|
@ -1721,6 +1725,8 @@ private:
|
|||
FHyperTwistCoachBrief RetainedGuidanceComparisonBrief;
|
||||
FHyperTwistTrainingDeck RetainedGuidanceComparisonDeck;
|
||||
FString RetainedGuidanceComparisonSelectedCaseId;
|
||||
FString CachedTemplateLaunchAnalyticsUserId;
|
||||
TArray<FHyperTwistTrainingTemplateLaunchScopedStats> CachedTemplateLaunchScopedStats;
|
||||
TArray<FHyperTwistCoachDashboardDecisionHistoryEntry> GuidanceDecisionHistoryEntries;
|
||||
TArray<FHyperTwistCoachDashboardHandoffHistoryEntry> HandoffHistoryEntries;
|
||||
TArray<FHyperTwistCoachDashboardTemplateLaunchHistoryEntry> TemplateLaunchHistoryEntries;
|
||||
|
|
|
|||
|
|
@ -270,6 +270,21 @@ public:
|
|||
const FString& ReferenceUtc
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository")
|
||||
static FHyperTwistTrainingTemplateLaunchScopedStats DeriveTemplateLaunchScopedStats(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& UserId,
|
||||
const FString& TemplateId,
|
||||
const FString& ReferenceUtc
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository")
|
||||
static TArray<FHyperTwistTrainingTemplateLaunchScopedStats> ListTemplateLaunchScopedStatsForUser(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
const FString& UserId,
|
||||
const FString& ReferenceUtc
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository")
|
||||
static FHyperTwistTrainingLearnerDeckStateSummary DeriveLearnerDeckStateSummary(
|
||||
const FHyperTwistTrainingRepositoryState& RepositoryState,
|
||||
|
|
|
|||
|
|
@ -1974,6 +1974,115 @@ struct FHyperTwistTrainingDeckScopedStats
|
|||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingTemplateLaunchScopedStats
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString UserId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString TemplateId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString ReferenceUtc;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString SourceTemplateId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString Title;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString FocusDeckId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString SourceReviewPlanId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingSessionTemplateKind TemplateKind =
|
||||
EHyperTwistTrainingSessionTemplateKind::Recovery;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy =
|
||||
EHyperTwistTrainingSelectionPolicy::Weighted;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 LaunchCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 CompletedRunCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 AbortedRunCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 OpenRunCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 ReviewBackedLaunchCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 CoachReviewLaunchCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 ShortReviewSetLaunchCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalAttemptCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalCompletedAttemptCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalSuccessCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalFailureCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalTimeoutCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalDNFCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 TotalMistakes = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 AverageRecommendedCaseCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 AverageFocusCaseCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 AverageCompletedAttemptCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
float OverallSuccessRate = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
float AverageMistakesPerOutcomeRun = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString LastLaunchAtUtc;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString LastCompletedAtUtc;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString LastSessionId;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return !UserId.IsEmpty() && !TemplateId.IsEmpty() && LaunchCount > 0;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingLearnerDeckStateSummary
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
# HyperTwist Phase 4 template launch scoped analytics packet
|
||||
|
||||
Created on `2026-05-06`
|
||||
|
||||
Status:
|
||||
|
||||
- first-party HyperTwist packet
|
||||
- bounded Phase `4` repository-backed template analytics slice
|
||||
|
||||
## Purpose
|
||||
|
||||
This packet turns persisted template-launch provenance into repo-backed per-template outcome analytics and surfaces those aggregates on the first-party dashboard.
|
||||
|
||||
The open tasks were:
|
||||
|
||||
- derive reusable per-template launch/outcome stats from stored run records
|
||||
- stop relying on ad hoc recap inspection to judge whether a template is paying off
|
||||
- surface those stats on the dashboard where operators launch and review template-backed runs
|
||||
|
||||
It is not:
|
||||
|
||||
- a template ranking rewrite packet
|
||||
- a new template editor or browser packet
|
||||
- a coach recommendation algorithm rewrite packet
|
||||
- a broader analytics export packet
|
||||
|
||||
## Scope
|
||||
|
||||
Bounded lane:
|
||||
|
||||
- add a persisted `FHyperTwistTrainingTemplateLaunchScopedStats` summary type
|
||||
- add repository helpers for deriving and listing per-template launch stats for a user
|
||||
- cache those repo-backed stats on the dashboard during template-history sync
|
||||
- append aggregate template outcome trends to the preferred active template launch section
|
||||
- append the same aggregate trends to template-launch history detail and template-backed run recap detail
|
||||
|
||||
Out of scope:
|
||||
|
||||
- changing which template wins dashboard launch priority
|
||||
- changing review-plan or method-drill derivation semantics
|
||||
- introducing a new dashboard page or analytics screen
|
||||
- feeding template outcome stats directly into coach handoff scoring rules yet
|
||||
|
||||
## Why this was the right next packet
|
||||
|
||||
The previous packet made template provenance durable in run history.
|
||||
|
||||
That still left one obvious gap:
|
||||
|
||||
- the repository now knew which template launched each run
|
||||
- but there was still no first-party aggregate view of how a template was actually performing across launches
|
||||
- and dashboard operators still had to infer template quality from one recap at a time
|
||||
|
||||
So the next honest move was:
|
||||
|
||||
- derive per-template scoped stats from the stored run records
|
||||
- keep the aggregation repo-backed and reusable
|
||||
- and surface the results on the existing dashboard template launch, history, and recap lanes
|
||||
|
||||
## What landed
|
||||
|
||||
Primary code changes:
|
||||
|
||||
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h`
|
||||
- added `FHyperTwistTrainingTemplateLaunchScopedStats`
|
||||
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h`
|
||||
- added repository APIs for deriving and listing template launch scoped stats
|
||||
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp`
|
||||
- aggregates per-template launch counts, outcome counts, aggregate success/failure totals, average launch sizing, average mistakes, and latest launch/completion timestamps from stored run records
|
||||
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h`
|
||||
- added cached template-launch analytics state plus a lookup helper
|
||||
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp`
|
||||
- caches template launch scoped stats when syncing repo-backed template history
|
||||
- appends aggregate template outcome lines to the preferred active template launch section
|
||||
- appends the same aggregate context to template launch history detail
|
||||
- appends aggregate template outcome context to template-backed run recap detail
|
||||
|
||||
## Product effect
|
||||
|
||||
Template quality is now visible without manual cross-referencing:
|
||||
|
||||
- the dashboard launch lane now shows how the preferred active template has performed across prior launches
|
||||
- template history navigation now carries aggregate per-template outcome context, not just one run snapshot
|
||||
- template-backed run recap now includes the broader historical template signal alongside the individual run outcome
|
||||
- later coach weighting or analytics work can reuse the same repo-backed template stats instead of re-scanning raw run records ad hoc
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- repository can derive per-template launch scoped stats from stored run records
|
||||
- dashboard caches repo-backed template stats during template history sync
|
||||
- preferred active template launch detail shows aggregate outcome trends when available
|
||||
- template-backed run history and run recap surfaces show aggregate template outcome context
|
||||
- full product build succeeds
|
||||
|
||||
## Validation checklist
|
||||
|
||||
1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor`
|
||||
2. confirm repository template stats populate for users with template-backed run history
|
||||
3. confirm dashboard `[Template Launch]` and `[Template Launch History]` sections show aggregate template outcome context
|
||||
4. confirm template-backed run recap shows the aggregate template signal beside the single-run outcome
|
||||
|
||||
That is the packet.
|
||||
Loading…
Add table
Reference in a new issue