From 4b53d209262de76c633addb81fbd3b253c550a45 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Sat, 9 May 2026 20:31:48 +0200 Subject: [PATCH] Expose dashboard last-attempt review and split-capture surfaces --- .../HyperTwistCoachDashboardWidget.cpp | 272 ++++++++++++++---- .../HyperTwistCoachDashboardWidget.h | 16 ++ .../HyperTwistTrainingTypes.h | 146 ++++++++++ ...RTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md | 4 +- ...TATUS_INSPECT_SURFACE_PACKET_2026-05-09.md | 56 ++++ 5 files changed, 438 insertions(+), 56 deletions(-) create mode 100644 docs/arch/HYPERTWIST_PHASE4_DASHBOARD_LIVE_LAST_ATTEMPT_REVIEW_AND_SPLIT_CAPTURE_STATUS_INSPECT_SURFACE_PACKET_2026-05-09.md diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 70bb7d5..96f058d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -10997,6 +10997,201 @@ UHyperTwistCoachDashboardWidget::GetDisplayedCurrentCaseDeckStatusInspectSurface return Surface; } +FHyperTwistTrainingCoachDashboardLastAttemptReviewSummaryInspectSurface +UHyperTwistCoachDashboardWidget::GetDisplayedLastAttemptReviewSummaryInspectSurface() const +{ + FHyperTwistTrainingCoachDashboardLastAttemptReviewSummaryInspectSurface Surface; + Surface.Headline = TEXT("Last attempt review summary"); + + const FHyperTwistTrainingAttempt* ReviewedAttempt = + SelectedAttemptIndex >= 0 && SelectedAttemptIndex < CachedRunState.Attempts.Num() + ? &CachedRunState.Attempts[SelectedAttemptIndex] + : nullptr; + const bool bHasSessionScopedLastStep = !LastStepResult.SessionSummary.TrainingSessionId.IsEmpty() + && LastStepResult.SessionSummary.TrainingSessionId == CachedRunState.Session.TrainingSessionId; + const FString ReviewedAttemptOrdinal = ReviewedAttempt != nullptr + ? FString::Printf(TEXT("%d/%d"), SelectedAttemptIndex + 1, CachedRunState.Attempts.Num()) + : TEXT("none"); + + Surface.AttemptOrdinal = ReviewedAttemptOrdinal; + Surface.AttemptId = ReviewedAttempt != nullptr && !ReviewedAttempt->AttemptId.IsEmpty() + ? ReviewedAttempt->AttemptId + : TEXT("n/a"); + Surface.CaseId = ReviewedAttempt != nullptr && !ReviewedAttempt->CaseId.IsEmpty() + ? ReviewedAttempt->CaseId + : TEXT("n/a"); + Surface.ResultLabel = ReviewedAttempt != nullptr + ? HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(ReviewedAttempt->Result) + : TEXT("n/a"); + Surface.CompletedAtUtc = ReviewedAttempt != nullptr && !ReviewedAttempt->CompletedAtUtc.IsEmpty() + ? ReviewedAttempt->CompletedAtUtc + : TEXT("n/a"); + Surface.LatestSubmissionLabel = bHasSessionScopedLastStep ? TEXT("yes") : TEXT("no"); + Surface.SelectedAttemptIndex = SelectedAttemptIndex; + Surface.TotalAttemptCount = CachedRunState.Attempts.Num(); + Surface.bHasReviewedAttempt = ReviewedAttempt != nullptr; + Surface.bLatestSubmissionMatchesActiveSession = bHasSessionScopedLastStep; + + Surface.StatusLine = !DisplayedLastAttemptReviewSummaryLine.IsEmpty() + ? DisplayedLastAttemptReviewSummaryLine + : (ReviewedAttempt != nullptr + ? FString::Printf( + TEXT("Attempt review: %s | case %s | result %s | completed at %s | latest submission: %s"), + *Surface.AttemptOrdinal, + *Surface.CaseId, + *Surface.ResultLabel, + *Surface.CompletedAtUtc, + *Surface.LatestSubmissionLabel) + : TEXT("Attempt review: no attempts recorded in the active run yet.")); + return Surface; +} + +FHyperTwistTrainingCoachDashboardLastAttemptTimingDerivedPhaseReviewInspectSurface +UHyperTwistCoachDashboardWidget::GetDisplayedLastAttemptTimingDerivedPhaseReviewInspectSurface() const +{ + FHyperTwistTrainingCoachDashboardLastAttemptTimingDerivedPhaseReviewInspectSurface Surface; + Surface.Headline = TEXT("Last attempt timing / derived-phase review"); + + const FHyperTwistTrainingAttempt* ReviewedAttempt = + SelectedAttemptIndex >= 0 && SelectedAttemptIndex < CachedRunState.Attempts.Num() + ? &CachedRunState.Attempts[SelectedAttemptIndex] + : nullptr; + FString ReviewedAttemptPhaseSummary = TEXT("none"); + if (ReviewedAttempt != nullptr && ReviewedAttempt->TimingBreakdown.DerivedPhaseTimings.Num() > 0) + { + TArray PhaseParts; + PhaseParts.Reserve(ReviewedAttempt->TimingBreakdown.DerivedPhaseTimings.Num()); + for (const FHyperTwistPhaseTiming& PhaseTiming : ReviewedAttempt->TimingBreakdown.DerivedPhaseTimings) + { + PhaseParts.Add(FString::Printf( + TEXT("%s %d-%d (%d ms)"), + PhaseTiming.PhaseKind.IsEmpty() ? TEXT("phase") : *PhaseTiming.PhaseKind, + PhaseTiming.StartedAtMs, + PhaseTiming.EndedAtMs, + PhaseTiming.DurationMs + )); + } + ReviewedAttemptPhaseSummary = FString::Join(PhaseParts, TEXT(" | ")); + } + + Surface.AttemptId = ReviewedAttempt != nullptr && !ReviewedAttempt->AttemptId.IsEmpty() + ? ReviewedAttempt->AttemptId + : TEXT("n/a"); + Surface.AppliedPenaltyLabel = ReviewedAttempt != nullptr + ? HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + ReviewedAttempt->TimingBreakdown.AppliedPenalty) + : TEXT("n/a"); + Surface.DerivedPhaseSummary = ReviewedAttemptPhaseSummary; + Surface.InspectionElapsedMs = + ReviewedAttempt != nullptr ? ReviewedAttempt->TimingBreakdown.InspectionElapsedMs : 0; + Surface.RawSolveTimeMs = + ReviewedAttempt != nullptr ? ReviewedAttempt->TimingBreakdown.RawSolveTimeMs : 0; + Surface.FinalTimeMs = + ReviewedAttempt != nullptr ? ReviewedAttempt->TimingBreakdown.FinalTimeMs : 0; + Surface.SplitCaptureCount = + ReviewedAttempt != nullptr ? ReviewedAttempt->TimingBreakdown.SplitCaptures.Num() : 0; + Surface.DerivedPhaseCount = + ReviewedAttempt != nullptr ? ReviewedAttempt->TimingBreakdown.DerivedPhaseTimings.Num() : 0; + Surface.bHasReviewedAttempt = ReviewedAttempt != nullptr; + Surface.bDidNotFinish = + ReviewedAttempt != nullptr && ReviewedAttempt->TimingBreakdown.bDidNotFinish; + + Surface.TimingLine = !DisplayedLastAttemptTimingLine.IsEmpty() + ? DisplayedLastAttemptTimingLine + : (ReviewedAttempt != nullptr + ? FString::Printf( + TEXT("Reviewed timing: inspection %d ms | raw solve %d ms | final %d ms | penalty %s | split captures %d | did-not-finish: %s"), + Surface.InspectionElapsedMs, + Surface.RawSolveTimeMs, + Surface.FinalTimeMs, + *Surface.AppliedPenaltyLabel, + Surface.SplitCaptureCount, + Surface.bDidNotFinish ? TEXT("yes") : TEXT("no")) + : TEXT("Reviewed timing: unavailable until the active run records an attempt.")); + Surface.DerivedPhaseLine = !DisplayedLastAttemptDerivedPhaseLine.IsEmpty() + ? DisplayedLastAttemptDerivedPhaseLine + : (ReviewedAttempt != nullptr + ? FString::Printf( + TEXT("Reviewed derived phases: %s"), + *Surface.DerivedPhaseSummary) + : TEXT("Reviewed derived phases: none yet.")); + return Surface; +} + +FHyperTwistTrainingCoachDashboardLiveSplitCaptureStatusInspectSurface +UHyperTwistCoachDashboardWidget::GetDisplayedLiveSplitCaptureStatusInspectSurface() const +{ + FHyperTwistTrainingCoachDashboardLiveSplitCaptureStatusInspectSurface Surface; + Surface.Headline = TEXT("Live split capture status"); + + const FHyperTwistTrainingTimingPolicy* TimingPolicy = FindActiveTimingPolicy(); + const int32 MaxManualSplitCaptureCount = GetMaxManualSplitCaptureCount(); + const FHyperTwistTrainingSplitPhaseDefinition* NextSplitPhaseDefinition = FindNextSplitPhaseToCapture(); + const FString NextSplitLabel = NextSplitPhaseDefinition == nullptr + ? TEXT("none") + : (NextSplitPhaseDefinition->Label.IsEmpty() + ? NextSplitPhaseDefinition->PhaseId + : NextSplitPhaseDefinition->Label); + FString CapturedSplitSummary = TEXT("none"); + if (LiveAttemptSplitCaptures.Num() > 0) + { + TArray CapturedSplitParts; + CapturedSplitParts.Reserve(LiveAttemptSplitCaptures.Num()); + for (const FHyperTwistTrainingSplitCapture& SplitCapture : LiveAttemptSplitCaptures) + { + const FString Label = SplitCapture.Label.IsEmpty() ? SplitCapture.PhaseId : SplitCapture.Label; + CapturedSplitParts.Add(FString::Printf(TEXT("%s@%d"), *Label, SplitCapture.TimestampMs)); + } + CapturedSplitSummary = FString::Join(CapturedSplitParts, TEXT(" | ")); + } + const FString FinalSplitLabel = + TimingPolicy != nullptr + && TimingPolicy->SplitPhases.IsValidIndex(MaxManualSplitCaptureCount) + ? (TimingPolicy->SplitPhases[MaxManualSplitCaptureCount].Label.IsEmpty() + ? TimingPolicy->SplitPhases[MaxManualSplitCaptureCount].PhaseId + : TimingPolicy->SplitPhases[MaxManualSplitCaptureCount].Label) + : TEXT("solve end"); + const bool bLiveInspectionPhase = IsLiveAttemptInspectionPhase(); + const bool bLiveSolvePhase = bHasLiveAttemptTimer + && LiveAttemptPhase == EHyperTwistCoachDashboardAttemptPhase::Solving; + + Surface.NextSplitLabel = NextSplitLabel; + Surface.FinalAutoCloseLabel = FinalSplitLabel; + Surface.CapturedSummary = CapturedSplitSummary; + Surface.CapturedSplitCount = LiveAttemptSplitCaptures.Num(); + Surface.MaxManualSplitCaptureCount = MaxManualSplitCaptureCount; + Surface.bHasConfiguredManualSplitPhases = MaxManualSplitCaptureCount > 0; + Surface.bHasLiveAttemptTimer = bHasLiveAttemptTimer; + Surface.bLiveInspectionPhase = bLiveInspectionPhase; + Surface.bLiveSolvePhase = bLiveSolvePhase; + + if (!DisplayedSplitCaptureStatusLine.IsEmpty()) + { + Surface.StatusLine = DisplayedSplitCaptureStatusLine; + } + else if (MaxManualSplitCaptureCount <= 0) + { + Surface.StatusLine = + TEXT("Split capture: no manual split phases are configured for this timing policy."); + } + else + { + const FString SplitStateLine = FString::Printf( + TEXT("Manual splits %d/%d | Next: %s | Final auto-close: %s | Captured: %s"), + Surface.CapturedSplitCount, + Surface.MaxManualSplitCaptureCount, + *Surface.NextSplitLabel, + *Surface.FinalAutoCloseLabel, + *Surface.CapturedSummary); + Surface.StatusLine = bLiveSolvePhase + ? SplitStateLine + TEXT(" | capture phase ends during solve; final phase closes on attempt stop") + : (bLiveInspectionPhase + ? SplitStateLine + TEXT(" | split capture unlocks after solve start") + : SplitStateLine + TEXT(" | start a live attempt to record manual split boundaries")); + } + return Surface; +} + bool UHyperTwistCoachDashboardWidget::SelectPreviousMethodDrillFollowUpHistoryEntry() { if (MethodDrillFollowUpHistoryEntries.Num() <= 0) @@ -22908,9 +23103,8 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() } if (LastAttemptTextBlock != nullptr) { - if (ReviewedAttempt != nullptr) - { - LastAttemptTextBlock->SetText(FText::FromString(FString::Printf( + DisplayedLastAttemptReviewSummaryLine = ReviewedAttempt != nullptr + ? FString::Printf( TEXT("Attempt review: %s | case %s | result %s | completed at %s | latest submission: %s"), *ReviewedAttemptOrdinal, ReviewedAttempt->CaseId.IsEmpty() ? TEXT("n/a") : *ReviewedAttempt->CaseId, @@ -22919,20 +23113,14 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() ? TEXT("n/a") : *ReviewedAttempt->CompletedAtUtc, bHasSessionScopedLastStep ? TEXT("yes") : TEXT("no") - ))); - } - else - { - LastAttemptTextBlock->SetText(FText::FromString( - TEXT("Attempt review: no attempts recorded in the active run yet.") - )); - } + ) + : TEXT("Attempt review: no attempts recorded in the active run yet."); + LastAttemptTextBlock->SetText(FText::FromString(DisplayedLastAttemptReviewSummaryLine)); } if (LastAttemptTimingTextBlock != nullptr) { - if (ReviewedAttempt != nullptr) - { - LastAttemptTimingTextBlock->SetText(FText::FromString(FString::Printf( + DisplayedLastAttemptTimingLine = ReviewedAttempt != nullptr + ? FString::Printf( TEXT("Reviewed timing: inspection %d ms | raw solve %d ms | final %d ms | penalty %s | split captures %d | did-not-finish: %s"), ReviewedAttempt->TimingBreakdown.InspectionElapsedMs, ReviewedAttempt->TimingBreakdown.RawSolveTimeMs, @@ -22940,30 +23128,19 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(ReviewedAttempt->TimingBreakdown.AppliedPenalty), ReviewedAttempt->TimingBreakdown.SplitCaptures.Num(), ReviewedAttempt->TimingBreakdown.bDidNotFinish ? TEXT("yes") : TEXT("no") - ))); - } - else - { - LastAttemptTimingTextBlock->SetText(FText::FromString( - TEXT("Reviewed timing: unavailable until the active run records an attempt.") - )); - } + ) + : TEXT("Reviewed timing: unavailable until the active run records an attempt."); + LastAttemptTimingTextBlock->SetText(FText::FromString(DisplayedLastAttemptTimingLine)); } if (LastAttemptPhaseTextBlock != nullptr) { - if (ReviewedAttempt != nullptr) - { - LastAttemptPhaseTextBlock->SetText(FText::FromString(FString::Printf( + DisplayedLastAttemptDerivedPhaseLine = ReviewedAttempt != nullptr + ? FString::Printf( TEXT("Reviewed derived phases: %s"), *ReviewedAttemptPhaseSummary - ))); - } - else - { - LastAttemptPhaseTextBlock->SetText(FText::FromString( - TEXT("Reviewed derived phases: none yet.") - )); - } + ) + : TEXT("Reviewed derived phases: none yet."); + LastAttemptPhaseTextBlock->SetText(FText::FromString(DisplayedLastAttemptDerivedPhaseLine)); } if (CurrentCaseHeaderTextBlock != nullptr) { @@ -23085,9 +23262,8 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { if (MaxManualSplitCaptureCount <= 0) { - SplitStatusTextBlock->SetText(FText::FromString( - TEXT("Split capture: no manual split phases are configured for this timing policy.") - )); + DisplayedSplitCaptureStatusLine = + TEXT("Split capture: no manual split phases are configured for this timing policy."); } else { @@ -23099,25 +23275,13 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() *FinalSplitLabel, *CapturedSplitSummary ); - if (bLiveSolvePhase) - { - SplitStatusTextBlock->SetText(FText::FromString( - SplitStateLine + TEXT(" | capture phase ends during solve; final phase closes on attempt stop") - )); - } - else if (bLiveInspectionPhase) - { - SplitStatusTextBlock->SetText(FText::FromString( - SplitStateLine + TEXT(" | split capture unlocks after solve start") - )); - } - else - { - SplitStatusTextBlock->SetText(FText::FromString( - SplitStateLine + TEXT(" | start a live attempt to record manual split boundaries") - )); - } + DisplayedSplitCaptureStatusLine = bLiveSolvePhase + ? SplitStateLine + TEXT(" | capture phase ends during solve; final phase closes on attempt stop") + : (bLiveInspectionPhase + ? SplitStateLine + TEXT(" | split capture unlocks after solve start") + : SplitStateLine + TEXT(" | start a live attempt to record manual split boundaries")); } + SplitStatusTextBlock->SetText(FText::FromString(DisplayedSplitCaptureStatusLine)); } if (AttemptTimeTextBox != nullptr) { diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 304a219..bc1e31e 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -827,6 +827,18 @@ public: FHyperTwistTrainingCoachDashboardCurrentCaseDeckStatusInspectSurface GetDisplayedCurrentCaseDeckStatusInspectSurface() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingCoachDashboardLastAttemptReviewSummaryInspectSurface + GetDisplayedLastAttemptReviewSummaryInspectSurface() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingCoachDashboardLastAttemptTimingDerivedPhaseReviewInspectSurface + GetDisplayedLastAttemptTimingDerivedPhaseReviewInspectSurface() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingCoachDashboardLiveSplitCaptureStatusInspectSurface + GetDisplayedLiveSplitCaptureStatusInspectSurface() const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool SelectPreviousMethodDrillFollowUpHistoryEntry(); @@ -1872,7 +1884,11 @@ private: FString DisplayedActiveRunSummaryLine; FString DisplayedCurrentCasePromptLine; FString DisplayedCurrentCaseNotationLine; + FString DisplayedLastAttemptReviewSummaryLine; + FString DisplayedLastAttemptTimingLine; + FString DisplayedLastAttemptDerivedPhaseLine; FString DisplayedAttemptInputStatusLine; + FString DisplayedSplitCaptureStatusLine; FString DisplayedTimerStatusLine; FString DisplayedRecommendedDeckPreviewLine; FString DisplayedFollowUpDeckPreviewLine; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 6ced625..7aa92dc 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -10821,6 +10821,152 @@ struct FHyperTwistTrainingCoachDashboardHandoffRecommendationInspectSurface } }; +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachDashboardLastAttemptReviewSummaryInspectSurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString AttemptOrdinal; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString AttemptId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ResultLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CompletedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LatestSubmissionLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SelectedAttemptIndex = INDEX_NONE; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalAttemptCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasReviewedAttempt = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bLatestSubmissionMatchesActiveSession = false; + + bool IsStructurallyValid() const + { + return !StatusLine.IsEmpty() + || bHasReviewedAttempt; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachDashboardLastAttemptTimingDerivedPhaseReviewInspectSurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TimingLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DerivedPhaseLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString AttemptId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString AppliedPenaltyLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DerivedPhaseSummary; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 InspectionElapsedMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RawSolveTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 FinalTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SplitCaptureCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DerivedPhaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasReviewedAttempt = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bDidNotFinish = false; + + bool IsStructurallyValid() const + { + return !TimingLine.IsEmpty() + || !DerivedPhaseLine.IsEmpty() + || bHasReviewedAttempt; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachDashboardLiveSplitCaptureStatusInspectSurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString NextSplitLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FinalAutoCloseLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CapturedSummary; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CapturedSplitCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 MaxManualSplitCaptureCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasConfiguredManualSplitPhases = 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; + + bool IsStructurallyValid() const + { + return !StatusLine.IsEmpty() + || bHasConfiguredManualSplitPhases + || bHasLiveAttemptTimer; + } +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface { diff --git a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md index 29c86fc..759bb43 100644 --- a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md +++ b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md @@ -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 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 latest landed bounded `Phase 4` packet stays on that same live `UHyperTwistCoachDashboardWidget` guidance-consumer lane and adds displayed last-attempt review summary, displayed last-attempt timing / derived-phase review, and live split-capture status inspect surfaces over already-derived live dashboard review/timing/split presentation state, so gameplay/Blueprint callers can inspect the active reviewed attempt line, reviewed timing and derived-phase lines, and live split-capture posture without scraping dashboard text blocks or reopening attempt-review or split-capture derivation + - the next clean move is to stay on that same live dashboard guidance-consumer lane and land the attempt-time input hint, live timer / split control label, and attempt result-action enablement 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 diff --git a/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_LIVE_LAST_ATTEMPT_REVIEW_AND_SPLIT_CAPTURE_STATUS_INSPECT_SURFACE_PACKET_2026-05-09.md b/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_LIVE_LAST_ATTEMPT_REVIEW_AND_SPLIT_CAPTURE_STATUS_INSPECT_SURFACE_PACKET_2026-05-09.md new file mode 100644 index 0000000..5491749 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_LIVE_LAST_ATTEMPT_REVIEW_AND_SPLIT_CAPTURE_STATUS_INSPECT_SURFACE_PACKET_2026-05-09.md @@ -0,0 +1,56 @@ +# HyperTwist Phase 4 dashboard live last-attempt review and split-capture 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 last-attempt review and split-capture presentation state directly to gameplay and Blueprint callers. + +## Why this packet exists + +The dashboard already derived: + +- the displayed last-attempt review summary line +- the displayed reviewed timing line +- the displayed reviewed derived-phase line +- the displayed split-capture status line + +But that live state still only surfaced through dashboard text blocks. Callers still had to scrape widget presentation or reopen last-attempt review and split-capture derivation to inspect what the dashboard was currently showing. + +## What changed + +- added `FHyperTwistTrainingCoachDashboardLastAttemptReviewSummaryInspectSurface` +- added `FHyperTwistTrainingCoachDashboardLastAttemptTimingDerivedPhaseReviewInspectSurface` +- added `FHyperTwistTrainingCoachDashboardLiveSplitCaptureStatusInspectSurface` +- added `GetDisplayedLastAttemptReviewSummaryInspectSurface()` +- added `GetDisplayedLastAttemptTimingDerivedPhaseReviewInspectSurface()` +- added `GetDisplayedLiveSplitCaptureStatusInspectSurface()` +- retained the exact displayed last-attempt review line, reviewed timing line, reviewed derived-phase line, and split-capture status line 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 reviewed-attempt summary line and selected attempt identity/posture +- the displayed reviewed timing line and reviewed derived-phase line +- the displayed live split-capture status line, including next split, final auto-close, and captured manual boundaries + +without scraping dashboard text blocks or reopening attempt-review / split-capture 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 attempt-time input hint, live timer / split control label, and attempt result-action enablement inspect surface group.