Expose dashboard active run, attempt timer, and current case surfaces

This commit is contained in:
axiomlogicnexus 2026-05-09 20:14:58 +02:00
parent 67ec0915cb
commit b0d89dbca4
5 changed files with 692 additions and 104 deletions

View file

@ -10725,6 +10725,278 @@ UHyperTwistCoachDashboardWidget::GetDisplayedRunActionLabelsInspectSurface() con
return Surface;
}
FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface
UHyperTwistCoachDashboardWidget::GetDisplayedActiveRunStatusInspectSurface() const
{
FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface Surface;
Surface.Headline = TEXT("Active run status");
const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid()
&& CachedRunState.ActiveDeck.IsStructurallyValid();
const FString ActiveDeckLabel = CachedRunState.ActiveDeck.Title.IsEmpty()
? (CachedRunState.ActiveDeck.DeckId.IsEmpty() ? TEXT("n/a") : CachedRunState.ActiveDeck.DeckId)
: CachedRunState.ActiveDeck.Title;
const FString ActiveCaseId = CachedRunState.CurrentSelection.TrainingCase.CaseId.IsEmpty()
? TEXT("n/a")
: CachedRunState.CurrentSelection.TrainingCase.CaseId;
Surface.TrainingSessionId = CachedRunState.Session.TrainingSessionId.IsEmpty()
? TEXT("n/a")
: CachedRunState.Session.TrainingSessionId;
Surface.SessionStateLabel =
HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.SessionState);
Surface.DeliveryModeLabel =
HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.Mode);
Surface.ActiveDeckLabel = ActiveDeckLabel;
Surface.ActiveCaseId = ActiveCaseId;
Surface.LastCaseId = CachedRunSummary.LastCaseId.IsEmpty()
? TEXT("n/a")
: CachedRunSummary.LastCaseId;
Surface.RemainingCaseCount = CachedRunState.RemainingCaseIds.Num();
Surface.AttemptCount = CachedRunSummary.AttemptCount;
Surface.CompletedAttemptCount = CachedRunSummary.CompletedAttemptCount;
Surface.SuccessCount = CachedRunSummary.SuccessCount;
Surface.FailureCount = CachedRunSummary.FailureCount;
Surface.TimeoutCount = CachedRunSummary.TimeoutCount;
Surface.DNFCount = CachedRunSummary.DNFCount;
Surface.BestTotalTimeMs = CachedRunSummary.BestTotalTimeMs;
Surface.AverageTotalTimeMs = CachedRunSummary.AverageTotalTimeMs;
Surface.bHasActiveRun = bHasActiveRun;
const bool bQueueRecoveryMemorySource =
HyperTwistCoachDashboardWidgetInternal::IsQueueRecoveryMemorySource(
CachedCoachPanelState.PreferredGuidanceSourceLabel);
Surface.bClosedLoopIdleDashboard =
IsCoachLoopActionabilityClosed()
&& (
bQueueRecoveryMemorySource
|| !CachedCoachPanelState.PreferredGuidanceSourceLabel.IsEmpty()
|| DisplayedCoachHandoffRecommendationKind != EHyperTwistTrainingCoachHandoffKind::None);
Surface.StatusLine = !DisplayedActiveRunStatusLine.IsEmpty()
? DisplayedActiveRunStatusLine
: (bHasActiveRun
? FString::Printf(
TEXT("Session: %s | State: %s | Deck: %s | Mode: %s | Current case: %s | Remaining: %d"),
*Surface.TrainingSessionId,
*Surface.SessionStateLabel,
*Surface.ActiveDeckLabel,
*Surface.DeliveryModeLabel,
*Surface.ActiveCaseId,
Surface.RemainingCaseCount)
: (Surface.bClosedLoopIdleDashboard
? TEXT("No active run. The coach-to-review loop is closed and idle.")
: TEXT("No active run. Queue and coach guidance are ready for launch from this screen.")));
Surface.SummaryLine = !DisplayedActiveRunSummaryLine.IsEmpty()
? DisplayedActiveRunSummaryLine
: FString::Printf(
TEXT("Attempts: %d total | %d completed | Success %d | Failure %d | Timeout %d | DNF %d | Best %d ms | Avg %d ms | Last case: %s"),
Surface.AttemptCount,
Surface.CompletedAttemptCount,
Surface.SuccessCount,
Surface.FailureCount,
Surface.TimeoutCount,
Surface.DNFCount,
Surface.BestTotalTimeMs,
Surface.AverageTotalTimeMs,
*Surface.LastCaseId);
return Surface;
}
FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface
UHyperTwistCoachDashboardWidget::GetDisplayedActiveAttemptTimerStatusInspectSurface() const
{
FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface Surface;
Surface.Headline = TEXT("Active attempt / timer status");
int32 ParsedAttemptOverrideMs = 0;
const bool bHasParsedAttemptOverride = TryParseAttemptTimeOverride(ParsedAttemptOverrideMs);
const int32 ResolvedSuccessTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Success);
const int32 ResolvedFailureTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Failure);
const int32 ResolvedTimeoutTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Timeout);
const int32 ResolvedDnfTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::DNF);
const FHyperTwistTrainingTimingPolicy* TimingPolicy = FindActiveTimingPolicy();
const bool bHasInspectionWindow = TimingPolicy != nullptr
&& TimingPolicy->bInspectionEnabled
&& TimingPolicy->InspectionDurationMs > 0;
const int32 InspectionLimitMs = bHasInspectionWindow ? FMath::Max(TimingPolicy->InspectionDurationMs, 0) : 0;
const bool bLiveInspectionPhase = IsLiveAttemptInspectionPhase();
const bool bLiveSolvePhase = bHasLiveAttemptTimer
&& LiveAttemptPhase == EHyperTwistCoachDashboardAttemptPhase::Solving;
const bool bRunningSolveTimeHidden =
TimingPolicy != nullptr && TimingPolicy->bHideRunningTime && bLiveSolvePhase;
const FString RunningSolveLabel =
bRunningSolveTimeHidden ? TEXT("hidden") : FString::FromInt(LiveAttemptSolveElapsedMs);
const EHyperTwistTrainingPenalty LiveInspectionPenalty =
(bHasInspectionWindow
&& TimingPolicy->bInspectionPenaltiesEnabled
&& LiveAttemptInspectionElapsedMs > InspectionLimitMs
&& LiveAttemptInspectionElapsedMs <= InspectionLimitMs + 2000)
? EHyperTwistTrainingPenalty::Plus2
: ((bHasInspectionWindow
&& TimingPolicy->bInspectionPenaltiesEnabled
&& LiveAttemptInspectionElapsedMs > InspectionLimitMs + 2000)
? EHyperTwistTrainingPenalty::DNF
: EHyperTwistTrainingPenalty::None);
const FString InspectionWindowLabel = bHasInspectionWindow
? FString::Printf(TEXT("inspection target %d ms"), InspectionLimitMs)
: TEXT("no inspection window");
Surface.AttemptOverrideMs = ParsedAttemptOverrideMs;
Surface.TargetTimeMs = CachedRunState.CurrentSelection.TrainingCase.TimeTargetMs;
Surface.AverageTotalTimeMs = CachedRunSummary.AverageTotalTimeMs;
Surface.ResolvedSuccessTimeMs = ResolvedSuccessTimeMs;
Surface.ResolvedFailureTimeMs = ResolvedFailureTimeMs;
Surface.ResolvedTimeoutTimeMs = ResolvedTimeoutTimeMs;
Surface.ResolvedDnfTimeMs = ResolvedDnfTimeMs;
Surface.InspectionLimitMs = InspectionLimitMs;
Surface.InspectionElapsedMs = LiveAttemptInspectionElapsedMs;
Surface.SolveElapsedMs = LiveAttemptSolveElapsedMs;
Surface.DisplayedElapsedMs = LiveAttemptDisplayedMs;
Surface.TimerPhaseLabel = bLiveInspectionPhase
? TEXT("inspection")
: (bLiveSolvePhase ? TEXT("solving") : TEXT("idle"));
Surface.RunningSolveLabel = RunningSolveLabel;
Surface.InspectionWindowLabel = InspectionWindowLabel;
Surface.PendingPenaltyLabel =
HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(LiveInspectionPenalty);
Surface.bHasParsedAttemptOverride = bHasParsedAttemptOverride;
Surface.bHasLiveAttemptTimer = bHasLiveAttemptTimer;
Surface.bLiveInspectionPhase = bLiveInspectionPhase;
Surface.bLiveSolvePhase = bLiveSolvePhase;
Surface.bHasInspectionWindow = bHasInspectionWindow;
Surface.bRunningSolveTimeHidden = bRunningSolveTimeHidden;
Surface.AttemptInputLine = !DisplayedAttemptInputStatusLine.IsEmpty()
? DisplayedAttemptInputStatusLine
: FString::Printf(
TEXT("Attempt ms input: %s | Target %d | Avg %d | Success %d | Failure %d | Timeout %d | DNF %d"),
bHasParsedAttemptOverride ? *FString::FromInt(ParsedAttemptOverrideMs) : TEXT("auto"),
Surface.TargetTimeMs,
Surface.AverageTotalTimeMs,
Surface.ResolvedSuccessTimeMs,
Surface.ResolvedFailureTimeMs,
Surface.ResolvedTimeoutTimeMs,
Surface.ResolvedDnfTimeMs);
Surface.TimerStatusLine = !DisplayedTimerStatusLine.IsEmpty()
? DisplayedTimerStatusLine
: (bLiveInspectionPhase
? FString::Printf(
TEXT("Phase: inspection | elapsed %d ms | remaining %d ms | pending penalty: %s | click Start Solve when ready"),
Surface.InspectionElapsedMs,
bHasInspectionWindow ? FMath::Max(InspectionLimitMs - LiveAttemptInspectionElapsedMs, 0) : 0,
*Surface.PendingPenaltyLabel)
: (bLiveSolvePhase
? FString::Printf(
TEXT("Phase: solving | inspection %d ms | solve %s ms | finish with Success / Failure / Timeout / DNF"),
Surface.InspectionElapsedMs,
*Surface.RunningSolveLabel)
: FString::Printf(
TEXT("Timer idle | %s | start a live attempt for less-synthetic run entry"),
*Surface.InspectionWindowLabel)));
return Surface;
}
FHyperTwistTrainingCoachDashboardCurrentCaseDeckStatusInspectSurface
UHyperTwistCoachDashboardWidget::GetDisplayedCurrentCaseDeckStatusInspectSurface() const
{
FHyperTwistTrainingCoachDashboardCurrentCaseDeckStatusInspectSurface Surface;
Surface.Headline = TEXT("Current case / deck status");
const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid()
&& CachedRunState.ActiveDeck.IsStructurallyValid();
const bool bHasRunnableCurrentCase =
bHasActiveRun && CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid();
const FHyperTwistTrainingCase& CurrentTrainingCase = CachedRunState.CurrentSelection.TrainingCase;
const FString ActiveDeckLabel = CachedRunState.ActiveDeck.Title.IsEmpty()
? (CachedRunState.ActiveDeck.DeckId.IsEmpty() ? TEXT("n/a") : CachedRunState.ActiveDeck.DeckId)
: CachedRunState.ActiveDeck.Title;
const FString ActiveCaseId = CurrentTrainingCase.CaseId.IsEmpty()
? TEXT("n/a")
: CurrentTrainingCase.CaseId;
const FString TagLine = CurrentTrainingCase.Tags.Num() > 0
? FString::Join(CurrentTrainingCase.Tags, TEXT(", "))
: TEXT("n/a");
Surface.ActiveDeckLabel = ActiveDeckLabel;
Surface.ActiveDeckId = CachedRunState.ActiveDeck.DeckId.IsEmpty()
? TEXT("n/a")
: CachedRunState.ActiveDeck.DeckId;
Surface.DeliveryModeLabel =
HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.Mode);
Surface.ActiveCaseId = ActiveCaseId;
Surface.PromptLabel = CurrentTrainingCase.PromptLabel.IsEmpty()
? TEXT("n/a")
: CurrentTrainingCase.PromptLabel;
Surface.PromptKindLabel =
HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CurrentTrainingCase.PromptKind);
Surface.SubsetId = CurrentTrainingCase.SubsetId.IsEmpty()
? TEXT("n/a")
: CurrentTrainingCase.SubsetId;
Surface.ScrambleNotation = CurrentTrainingCase.ScrambleNotation.IsEmpty()
? TEXT("n/a")
: CurrentTrainingCase.ScrambleNotation;
Surface.CanonicalNotation = CurrentTrainingCase.CanonicalNotation.IsEmpty()
? TEXT("n/a")
: CurrentTrainingCase.CanonicalNotation;
Surface.TagLine = TagLine;
Surface.TimeTargetMs = CurrentTrainingCase.TimeTargetMs;
Surface.SelectionAttemptCount = CachedRunState.CurrentSelection.AttemptCount;
Surface.SelectionSuccessCount = CachedRunState.CurrentSelection.SuccessCount;
Surface.RemainingCaseCount = CachedRunState.RemainingCaseIds.Num();
Surface.bHasActiveRun = bHasActiveRun;
Surface.bHasRunnableCurrentCase = bHasRunnableCurrentCase;
const bool bQueueRecoveryMemorySource =
HyperTwistCoachDashboardWidgetInternal::IsQueueRecoveryMemorySource(
CachedCoachPanelState.PreferredGuidanceSourceLabel);
Surface.bClosedLoopIdleDashboard =
IsCoachLoopActionabilityClosed()
&& (
bQueueRecoveryMemorySource
|| !CachedCoachPanelState.PreferredGuidanceSourceLabel.IsEmpty()
|| DisplayedCoachHandoffRecommendationKind != EHyperTwistTrainingCoachHandoffKind::None);
Surface.StatusLine = !DisplayedActiveRunStatusLine.IsEmpty()
? DisplayedActiveRunStatusLine
: (bHasActiveRun
? FString::Printf(
TEXT("Session: %s | State: %s | Deck: %s | Mode: %s | Current case: %s | Remaining: %d"),
CachedRunState.Session.TrainingSessionId.IsEmpty()
? TEXT("n/a")
: *CachedRunState.Session.TrainingSessionId,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.SessionState),
*Surface.ActiveDeckLabel,
*Surface.DeliveryModeLabel,
*Surface.ActiveCaseId,
Surface.RemainingCaseCount)
: (Surface.bClosedLoopIdleDashboard
? TEXT("No active run. The coach-to-review loop is closed and idle.")
: TEXT("No active run. Queue and coach guidance are ready for launch from this screen.")));
Surface.PromptLine = !DisplayedCurrentCasePromptLine.IsEmpty()
? DisplayedCurrentCasePromptLine
: (bHasRunnableCurrentCase
? FString::Printf(
TEXT("Prompt: %s | Kind: %s | Subset: %s | Target: %d ms | Attempts on selection: %d | Successes on selection: %d"),
*Surface.PromptLabel,
*Surface.PromptKindLabel,
*Surface.SubsetId,
Surface.TimeTargetMs,
Surface.SelectionAttemptCount,
Surface.SelectionSuccessCount)
: TEXT("Prompt: no active current case is loaded."));
Surface.NotationLine = !DisplayedCurrentCaseNotationLine.IsEmpty()
? DisplayedCurrentCaseNotationLine
: (bHasRunnableCurrentCase
? FString::Printf(
TEXT("Scramble: %s\nCanonical: %s\nTags: %s"),
*Surface.ScrambleNotation,
*Surface.CanonicalNotation,
*Surface.TagLine)
: TEXT("Scramble / notation: unavailable until a run is active."));
return Surface;
}
bool UHyperTwistCoachDashboardWidget::SelectPreviousMethodDrillFollowUpHistoryEntry()
{
if (MethodDrillFollowUpHistoryEntries.Num() <= 0)
@ -22095,37 +22367,39 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
ActiveRunHeaderTextBlock->SetText(FText::FromString(TEXT("[Active Run]")));
}
const FString ActiveRunLine = bHasActiveRun
? FString::Printf(
TEXT("Session: %s | State: %s | Deck: %s | Mode: %s | Current case: %s | Remaining: %d"),
*CachedRunState.Session.TrainingSessionId,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.SessionState),
*ActiveDeckLabel,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.Mode),
*ActiveCaseId,
CachedRunState.RemainingCaseIds.Num()
)
: (bHasClosedLoopIdleCoachActionability
? TEXT("No active run. The coach-to-review loop is closed and idle.")
: TEXT("No active run. Queue and coach guidance are ready for launch from this screen."));
DisplayedActiveRunStatusLine = ActiveRunLine;
if (ActiveRunStatusTextBlock != nullptr)
{
const FString ActiveRunLine = bHasActiveRun
? FString::Printf(
TEXT("Session: %s | State: %s | Deck: %s | Mode: %s | Current case: %s | Remaining: %d"),
*CachedRunState.Session.TrainingSessionId,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.SessionState),
*ActiveDeckLabel,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.Mode),
*ActiveCaseId,
CachedRunState.RemainingCaseIds.Num()
)
: (bHasClosedLoopIdleCoachActionability
? TEXT("No active run. The coach-to-review loop is closed and idle.")
: TEXT("No active run. Queue and coach guidance are ready for launch from this screen."));
ActiveRunStatusTextBlock->SetText(FText::FromString(ActiveRunLine));
ActiveRunStatusTextBlock->SetText(FText::FromString(DisplayedActiveRunStatusLine));
}
DisplayedActiveRunSummaryLine = FString::Printf(
TEXT("Attempts: %d total | %d completed | Success %d | Failure %d | Timeout %d | DNF %d | Best %d ms | Avg %d ms | Last case: %s"),
CachedRunSummary.AttemptCount,
CachedRunSummary.CompletedAttemptCount,
CachedRunSummary.SuccessCount,
CachedRunSummary.FailureCount,
CachedRunSummary.TimeoutCount,
CachedRunSummary.DNFCount,
CachedRunSummary.BestTotalTimeMs,
CachedRunSummary.AverageTotalTimeMs,
CachedRunSummary.LastCaseId.IsEmpty() ? TEXT("n/a") : *CachedRunSummary.LastCaseId
);
if (ActiveRunSummaryTextBlock != nullptr)
{
ActiveRunSummaryTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Attempts: %d total | %d completed | Success %d | Failure %d | Timeout %d | DNF %d | Best %d ms | Avg %d ms | Last case: %s"),
CachedRunSummary.AttemptCount,
CachedRunSummary.CompletedAttemptCount,
CachedRunSummary.SuccessCount,
CachedRunSummary.FailureCount,
CachedRunSummary.TimeoutCount,
CachedRunSummary.DNFCount,
CachedRunSummary.BestTotalTimeMs,
CachedRunSummary.AverageTotalTimeMs,
CachedRunSummary.LastCaseId.IsEmpty() ? TEXT("n/a") : *CachedRunSummary.LastCaseId
)));
ActiveRunSummaryTextBlock->SetText(FText::FromString(DisplayedActiveRunSummaryLine));
}
if (RunRecapHeaderTextBlock != nullptr)
{
@ -22695,47 +22969,49 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
CurrentCaseHeaderTextBlock->SetText(FText::FromString(TEXT("[Current Case]")));
}
FString CurrentCasePromptLine;
if (bHasRunnableCurrentCase)
{
CurrentCasePromptLine = FString::Printf(
TEXT("Prompt: %s | Kind: %s | Subset: %s | Target: %d ms | Attempts on selection: %d | Successes on selection: %d"),
CurrentTrainingCase.PromptLabel.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.PromptLabel,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CurrentTrainingCase.PromptKind),
CurrentTrainingCase.SubsetId.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.SubsetId,
CurrentTrainingCase.TimeTargetMs,
CachedRunState.CurrentSelection.AttemptCount,
CachedRunState.CurrentSelection.SuccessCount
);
}
else
{
CurrentCasePromptLine = TEXT("Prompt: no active current case is loaded.");
}
DisplayedCurrentCasePromptLine = CurrentCasePromptLine;
if (CurrentCasePromptTextBlock != nullptr)
{
if (bHasRunnableCurrentCase)
{
CurrentCasePromptTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Prompt: %s | Kind: %s | Subset: %s | Target: %d ms | Attempts on selection: %d | Successes on selection: %d"),
CurrentTrainingCase.PromptLabel.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.PromptLabel,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CurrentTrainingCase.PromptKind),
CurrentTrainingCase.SubsetId.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.SubsetId,
CurrentTrainingCase.TimeTargetMs,
CachedRunState.CurrentSelection.AttemptCount,
CachedRunState.CurrentSelection.SuccessCount
)));
}
else
{
CurrentCasePromptTextBlock->SetText(FText::FromString(
TEXT("Prompt: no active current case is loaded.")
));
}
CurrentCasePromptTextBlock->SetText(FText::FromString(DisplayedCurrentCasePromptLine));
}
FString CurrentCaseNotationLine;
if (bHasRunnableCurrentCase)
{
const FString TagLine = CurrentTrainingCase.Tags.Num() > 0
? FString::Join(CurrentTrainingCase.Tags, TEXT(", "))
: TEXT("n/a");
CurrentCaseNotationLine = FString::Printf(
TEXT("Scramble: %s\nCanonical: %s\nTags: %s"),
CurrentTrainingCase.ScrambleNotation.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.ScrambleNotation,
CurrentTrainingCase.CanonicalNotation.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.CanonicalNotation,
*TagLine
);
}
else
{
CurrentCaseNotationLine = TEXT("Scramble / notation: unavailable until a run is active.");
}
DisplayedCurrentCaseNotationLine = CurrentCaseNotationLine;
if (CurrentCaseNotationTextBlock != nullptr)
{
if (bHasRunnableCurrentCase)
{
const FString TagLine = CurrentTrainingCase.Tags.Num() > 0
? FString::Join(CurrentTrainingCase.Tags, TEXT(", "))
: TEXT("n/a");
CurrentCaseNotationTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Scramble: %s\nCanonical: %s\nTags: %s"),
CurrentTrainingCase.ScrambleNotation.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.ScrambleNotation,
CurrentTrainingCase.CanonicalNotation.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.CanonicalNotation,
*TagLine
)));
}
else
{
CurrentCaseNotationTextBlock->SetText(FText::FromString(
TEXT("Scramble / notation: unavailable until a run is active.")
));
}
CurrentCaseNotationTextBlock->SetText(FText::FromString(DisplayedCurrentCaseNotationLine));
}
if (TransitionHeaderTextBlock != nullptr)
{
@ -22749,55 +23025,57 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
AttemptControlHeaderTextBlock->SetText(FText::FromString(TEXT("[Attempt Controls]")));
}
DisplayedAttemptInputStatusLine = FString::Printf(
TEXT("Attempt ms input: %s | Target %d | Avg %d | Success %d | Failure %d | Timeout %d | DNF %d"),
bHasParsedAttemptOverride ? *FString::FromInt(ParsedAttemptOverrideMs) : TEXT("auto"),
CurrentTrainingCase.TimeTargetMs,
CachedRunSummary.AverageTotalTimeMs,
ResolvedSuccessTimeMs,
ResolvedFailureTimeMs,
ResolvedTimeoutTimeMs,
ResolvedDnfTimeMs
);
if (AttemptInputStatusTextBlock != nullptr)
{
AttemptInputStatusTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Attempt ms input: %s | Target %d | Avg %d | Success %d | Failure %d | Timeout %d | DNF %d"),
bHasParsedAttemptOverride ? *FString::FromInt(ParsedAttemptOverrideMs) : TEXT("auto"),
CurrentTrainingCase.TimeTargetMs,
CachedRunSummary.AverageTotalTimeMs,
ResolvedSuccessTimeMs,
ResolvedFailureTimeMs,
ResolvedTimeoutTimeMs,
ResolvedDnfTimeMs
)));
AttemptInputStatusTextBlock->SetText(FText::FromString(DisplayedAttemptInputStatusLine));
}
if (TimerHeaderTextBlock != nullptr)
{
TimerHeaderTextBlock->SetText(FText::FromString(TEXT("[Live Timer]")));
}
if (bLiveInspectionPhase)
{
const int32 RemainingInspectionMs = bHasInspectionWindow
? FMath::Max(InspectionLimitMs - LiveAttemptInspectionElapsedMs, 0)
: 0;
DisplayedTimerStatusLine = FString::Printf(
TEXT("Phase: inspection | elapsed %d ms | remaining %d ms | pending penalty: %s | click Start Solve when ready"),
LiveAttemptInspectionElapsedMs,
RemainingInspectionMs,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(LiveInspectionPenalty)
);
}
else if (bLiveSolvePhase)
{
DisplayedTimerStatusLine = FString::Printf(
TEXT("Phase: solving | inspection %d ms | solve %s ms | finish with Success / Failure / Timeout / DNF"),
LiveAttemptInspectionElapsedMs,
*RunningSolveLabel
);
}
else
{
const FString InspectionLabel = bHasInspectionWindow
? FString::Printf(TEXT("inspection target %d ms"), InspectionLimitMs)
: TEXT("no inspection window");
DisplayedTimerStatusLine = FString::Printf(
TEXT("Timer idle | %s | start a live attempt for less-synthetic run entry"),
*InspectionLabel
);
}
if (TimerStatusTextBlock != nullptr)
{
if (bLiveInspectionPhase)
{
const int32 RemainingInspectionMs = bHasInspectionWindow
? FMath::Max(InspectionLimitMs - LiveAttemptInspectionElapsedMs, 0)
: 0;
TimerStatusTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Phase: inspection | elapsed %d ms | remaining %d ms | pending penalty: %s | click Start Solve when ready"),
LiveAttemptInspectionElapsedMs,
RemainingInspectionMs,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(LiveInspectionPenalty)
)));
}
else if (bLiveSolvePhase)
{
TimerStatusTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Phase: solving | inspection %d ms | solve %s ms | finish with Success / Failure / Timeout / DNF"),
LiveAttemptInspectionElapsedMs,
*RunningSolveLabel
)));
}
else
{
const FString InspectionLabel = bHasInspectionWindow
? FString::Printf(TEXT("inspection target %d ms"), InspectionLimitMs)
: TEXT("no inspection window");
TimerStatusTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Timer idle | %s | start a live attempt for less-synthetic run entry"),
*InspectionLabel
)));
}
TimerStatusTextBlock->SetText(FText::FromString(DisplayedTimerStatusLine));
}
if (SplitHeaderTextBlock != nullptr)
{

View file

@ -815,6 +815,18 @@ public:
FHyperTwistTrainingCoachDashboardRunActionLabelsInspectSurface
GetDisplayedRunActionLabelsInspectSurface() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard")
FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface
GetDisplayedActiveRunStatusInspectSurface() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard")
FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface
GetDisplayedActiveAttemptTimerStatusInspectSurface() const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard")
FHyperTwistTrainingCoachDashboardCurrentCaseDeckStatusInspectSurface
GetDisplayedCurrentCaseDeckStatusInspectSurface() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool SelectPreviousMethodDrillFollowUpHistoryEntry();
@ -1856,6 +1868,12 @@ private:
FString DisplayedCoachHandoffRecommendationDetailLine;
FString DisplayedCoachHandoffStatusLine;
FString DisplayedTransitionStatusLine;
FString DisplayedActiveRunStatusLine;
FString DisplayedActiveRunSummaryLine;
FString DisplayedCurrentCasePromptLine;
FString DisplayedCurrentCaseNotationLine;
FString DisplayedAttemptInputStatusLine;
FString DisplayedTimerStatusLine;
FString DisplayedRecommendedDeckPreviewLine;
FString DisplayedFollowUpDeckPreviewLine;
FString DisplayedContinueRunLabel;

View file

@ -10821,6 +10821,242 @@ struct FHyperTwistTrainingCoachDashboardHandoffRecommendationInspectSurface
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString Headline;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString StatusLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SummaryLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString TrainingSessionId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SessionStateLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeliveryModeLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ActiveDeckLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ActiveCaseId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LastCaseId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 RemainingCaseCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 AttemptCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 CompletedAttemptCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 SuccessCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 FailureCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TimeoutCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DNFCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 BestTotalTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 AverageTotalTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasActiveRun = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bClosedLoopIdleDashboard = false;
bool IsStructurallyValid() const
{
return !StatusLine.IsEmpty()
|| !SummaryLine.IsEmpty()
|| bHasActiveRun;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString Headline;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString AttemptInputLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString TimerStatusLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString TimerPhaseLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString RunningSolveLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString InspectionWindowLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString PendingPenaltyLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 AttemptOverrideMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TargetTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 AverageTotalTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ResolvedSuccessTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ResolvedFailureTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ResolvedTimeoutTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ResolvedDnfTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InspectionLimitMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 InspectionElapsedMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 SolveElapsedMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 DisplayedElapsedMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasParsedAttemptOverride = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasLiveAttemptTimer = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bLiveInspectionPhase = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bLiveSolvePhase = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasInspectionWindow = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bRunningSolveTimeHidden = false;
bool IsStructurallyValid() const
{
return !AttemptInputLine.IsEmpty()
|| !TimerStatusLine.IsEmpty()
|| bHasLiveAttemptTimer;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachDashboardCurrentCaseDeckStatusInspectSurface
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString Headline;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString StatusLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString PromptLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString NotationLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ActiveDeckLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ActiveDeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeliveryModeLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ActiveCaseId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString PromptLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString PromptKindLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString SubsetId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ScrambleNotation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString CanonicalNotation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString TagLine;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TimeTargetMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 SelectionAttemptCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 SelectionSuccessCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 RemainingCaseCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasActiveRun = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bHasRunnableCurrentCase = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bClosedLoopIdleDashboard = false;
bool IsStructurallyValid() const
{
return !StatusLine.IsEmpty()
|| !PromptLine.IsEmpty()
|| !NotationLine.IsEmpty()
|| bHasRunnableCurrentCase
|| bHasActiveRun;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCoachDashboardHandoffStatusInspectSurface
{

View file

@ -69,8 +69,8 @@ Current correction call from the source-exposed audit and current execution refr
- the latest landed bounded `Phase 4` packet closes the retained-idle comparison/outcome actionability gap at the end of the recognition-assisted coach-to-review continuity lane, and the final retained-surface confirmation pass now leaves that closure lane functionally complete
- the retained idle widget surface now keeps retained comparison and decision history inspectable without continuing to advertise live launch or guidance-outcome actions once the coach-to-review loop has already collapsed to truthful closed-loop idle
- the now-complete deliberate `UHyperTwistTrainingRuntimeLibrary` world-context consumer lane reaches through active generated-mode selector option owned-execution split-phase current-selection merged ordered roster phase-id lookup, normalized-order lookup, and evaluation-order lookup inspect surfaces, so gameplay/Blueprint callers can inspect one retained merged-roster row directly by phase id, normalized order, or evaluation order without scanning the merged roster array or re-running inspect derivation details directly
- the latest landed bounded `Phase 4` packet stays on that same live `UHyperTwistCoachDashboardWidget` guidance-consumer lane and adds displayed coach handoff status, displayed transition status, and displayed continuation / queued / follow-up / recommended run action-label inspect surfaces over already-derived live dashboard transition and action-label state, so gameplay/Blueprint callers can inspect the current transition posture, coach handoff availability line, and surfaced launch/control labels without scraping dashboard text blocks or reopening transition/handoff derivation
- the next clean move is to stay on that same live dashboard guidance-consumer lane and land the displayed active-run status, active-attempt/timer status, and current-case / current-deck status inspect surface group rather than drifting back into backend continuity or reopening the already-complete retained-history seam
- the latest landed bounded `Phase 4` packet stays on that same live `UHyperTwistCoachDashboardWidget` guidance-consumer lane and adds displayed active-run status, displayed active-attempt/timer status, and displayed current-case / current-deck status inspect surfaces over already-derived live dashboard run-state, timer-state, and current-selection presentation state, so gameplay/Blueprint callers can inspect the current active-run line, run-summary line, attempt-input/timer posture, and current-case/current-deck presentation without scraping dashboard text blocks or reopening run/timer/current-case derivation
- the next clean move is to stay on that same live dashboard guidance-consumer lane and land the displayed last-attempt review summary, displayed last-attempt timing / derived-phase review, and live split-capture status inspect surface group rather than drifting back into backend continuity or reopening the already-complete retained-history seam
- the current execution-discipline rule that broad refactor / monolith-splitting work should not interrupt the active bounded roadmap packet unless structure is actually blocking it
## Current execution-reality references

View file

@ -0,0 +1,56 @@
# HyperTwist Phase 4 dashboard live active-run, attempt-timer, and current-case / current-deck status inspect surface packet
- bounded Phase `4` dashboard-consumer slice
- date: `2026-05-09`
## Intent
Stay on the deliberate live `UHyperTwistCoachDashboardWidget` guidance-consumer lane and expose the remaining live active-run, attempt/timer, and current-case/current-deck presentation state directly to gameplay and Blueprint callers.
## Why this packet exists
The dashboard already derived:
- the displayed active-run status line
- the displayed active-run summary line
- the displayed attempt-input and live-timer status lines
- the displayed current-case prompt and notation lines
But that live state still only surfaced through dashboard text blocks. Callers still had to scrape widget presentation or reopen active-run / timer / current-case derivation to inspect what the dashboard was currently showing.
## What changed
- added `FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface`
- added `FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface`
- added `FHyperTwistTrainingCoachDashboardCurrentCaseDeckStatusInspectSurface`
- added `GetDisplayedActiveRunStatusInspectSurface()`
- added `GetDisplayedActiveAttemptTimerStatusInspectSurface()`
- added `GetDisplayedCurrentCaseDeckStatusInspectSurface()`
- retained the exact displayed active-run status/summary lines, attempt-input line, timer-status line, and current-case prompt/notation lines inside the widget so the new inspect surfaces report the same live strings the dashboard is already presenting
- kept the packet bounded to `UHyperTwistCoachDashboardWidget` plus shared surface types, with no dashboard redesign and no backend continuity changes
## Files
- `C:\HyperTwist\UnrealHyperTwist\Source\UnrealHyperTwist\Public\HyperTwistTraining\HyperTwistTrainingTypes.h`
- `C:\HyperTwist\UnrealHyperTwist\Source\UnrealHyperTwist\Public\HyperTwistTraining\HyperTwistCoachDashboardWidget.h`
- `C:\HyperTwist\UnrealHyperTwist\Source\UnrealHyperTwist\Private\HyperTwistTraining\HyperTwistCoachDashboardWidget.cpp`
- `C:\HyperTwist\docs\HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md`
## Outcome
Gameplay and Blueprint callers can now inspect:
- the displayed active-run line and summarized active-run attempt totals
- the displayed attempt-input posture and live timer phase/status line
- the displayed current-case prompt/notation presentation plus the active current-deck context
without scraping dashboard text blocks or reopening active-run / timer / current-case derivation.
## Validation
- full `Development Editor | Win64` MSBuild on `UnrealHyperTwist.sln`
- passed with `0 Warning(s), 0 Error(s)`
## Next clean slice
Stay on the same live dashboard guidance-consumer lane and land the displayed last-attempt review summary, displayed last-attempt timing / derived-phase review, and live split-capture status inspect surface group.