diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index e7aa20f..6104f4e 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -264,6 +264,55 @@ namespace HyperTwistCoachDashboardWidgetInternal } } + int32 ScoreTemplateForDashboardLaunch(const FHyperTwistTrainingSessionTemplate& Template) + { + if (!Template.IsStructurallyValid()) + { + return 0; + } + + int32 Score = 0; + switch (Template.TemplateKind) + { + case EHyperTwistTrainingSessionTemplateKind::ReviewPlan: + Score = 500; + break; + case EHyperTwistTrainingSessionTemplateKind::Coach: + Score = Template.bPreferReviewRun || Template.bCoachReview ? 450 : 0; + break; + case EHyperTwistTrainingSessionTemplateKind::Recovery: + Score = 400; + break; + default: + Score = 0; + break; + } + + if (Score <= 0) + { + return 0; + } + + if (Template.bPreferReviewRun) + { + Score += 30; + } + if (!Template.SourceReviewPlanId.IsEmpty()) + { + Score += 20; + } + if (Template.bUserCreated) + { + Score += 10; + } + if (Template.bUserCustomized) + { + Score += 5; + } + + return Score; + } + EMethodDrillRecoveryMemoryBias ResolveMethodDrillRecoveryMemoryBias( const int32 HistoryCount, const int32 StrongCount, @@ -3447,6 +3496,43 @@ FHyperTwistTrainingDeck UHyperTwistCoachDashboardWidget::PreviewFollowUpCoachDec return Deck; } +FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartPreferredActiveSessionTemplateRun() +{ + if (bHasLiveAttemptTimer || CachedRunState.Session.IsStructurallyValid()) + { + UpdateDashboardPresentation(); + return FHyperTwistTrainingRunState(); + } + + const FHyperTwistTrainingSessionTemplate* PreferredTemplate = FindPreferredDashboardTemplateLaunch(); + if (PreferredTemplate == nullptr) + { + UpdateDashboardPresentation(); + return FHyperTwistTrainingRunState(); + } + + const FString ResolvedUserId = PreferredTemplate->UserId.IsEmpty() + ? DefaultUserId + : PreferredTemplate->UserId; + const FString LaunchSessionId = FString::Printf( + TEXT("%s_template_%s_%s"), + *DefaultSessionId, + *PreferredTemplate->TemplateId, + *FGuid::NewGuid().ToString(EGuidFormats::Digits) + ); + LastStepResult = FHyperTwistTrainingRunStepResult(); + const FHyperTwistTrainingRunState RunState = StartTrainingRunFromTemplate( + PreferredTemplate->TemplateId, + ResolvedUserId, + LaunchSessionId + ); + SyncRetainedRunRecap(); + SyncSelectedQueueEntry(); + SyncSelectedAttempt(); + UpdateDashboardPresentation(); + return RunState; +} + bool UHyperTwistCoachDashboardWidget::SelectPreviousGuidancePreviewCase() { const FHyperTwistTrainingDeck* Deck = GetActiveGuidancePreviewDeck(); @@ -4439,6 +4525,11 @@ void UHyperTwistCoachDashboardWidget::HandleStartRecommendedRunClicked() UpdateDashboardPresentation(); } +void UHyperTwistCoachDashboardWidget::HandleStartTemplateLaunchClicked() +{ + StartPreferredActiveSessionTemplateRun(); +} + void UHyperTwistCoachDashboardWidget::HandlePreviewRecommendedClicked() { PreviewRecommendedCoachDeck(); @@ -4909,6 +5000,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachHandoffRecommendationDetail"), 8 ); + TemplateLaunchHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachTemplateLaunchHeader"), + 2 + ); + TemplateLaunchStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachTemplateLaunchStatus"), + 2 + ); + TemplateLaunchDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachTemplateLaunchDetail"), + 8 + ); HandoffHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -5175,6 +5284,17 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() StartRecommendedRunLabelRaw ); StartRecommendedRunButtonLabel = StartRecommendedRunLabelRaw; + + UTextBlock* StartTemplateLaunchLabelRaw = nullptr; + StartTemplateLaunchButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + CoachHandoffActionRow, + TEXT("StartTemplateLaunchButton"), + TEXT("StartTemplateLaunchButtonLabel"), + TEXT("Launch Template"), + StartTemplateLaunchLabelRaw + ); + StartTemplateLaunchButtonLabel = StartTemplateLaunchLabelRaw; } UHorizontalBox* HandoffHistoryNavRow = WidgetTree->ConstructWidget( UHorizontalBox::StaticClass(), @@ -6173,6 +6293,12 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() { StartRecommendedRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleStartRecommendedRunClicked); } + if (StartTemplateLaunchButton != nullptr) + { + StartTemplateLaunchButton->OnClicked.AddDynamic( + this, + &UHyperTwistCoachDashboardWidget::HandleStartTemplateLaunchClicked); + } if (PreviewRecommendedButton != nullptr) { PreviewRecommendedButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviewRecommendedClicked); @@ -6620,6 +6746,24 @@ const FHyperTwistCoachBrief* UHyperTwistCoachDashboardWidget::GetActiveGuidanceP } } +const FHyperTwistTrainingSessionTemplate* UHyperTwistCoachDashboardWidget::FindPreferredDashboardTemplateLaunch() const +{ + const FHyperTwistTrainingSessionTemplate* PreferredTemplate = nullptr; + int32 PreferredScore = TNumericLimits::Lowest(); + for (const FHyperTwistTrainingSessionTemplate& SessionTemplate : CachedActiveTrainingSessionTemplates) + { + const int32 TemplateScore = + HyperTwistCoachDashboardWidgetInternal::ScoreTemplateForDashboardLaunch(SessionTemplate); + if (TemplateScore > PreferredScore) + { + PreferredTemplate = &SessionTemplate; + PreferredScore = TemplateScore; + } + } + + return PreferredScore > 0 ? PreferredTemplate : nullptr; +} + void UHyperTwistCoachDashboardWidget::CaptureActiveGuidanceLaunchContext( const FString& SessionId, const FHyperTwistTrainingDeck& Deck, @@ -10752,6 +10896,105 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() const bool bDisplayedGuidanceComparisonSingleCase = bHasActiveGuidanceComparison ? bActiveGuidanceLaunchSingleCase : (bHasDisplayedRetainedGuidanceComparison ? bRetainedGuidanceComparisonSingleCase : false); + const FHyperTwistTrainingSessionTemplate* PreferredTemplateLaunch = + FindPreferredDashboardTemplateLaunch(); + int32 EligibleTemplateLaunchCount = 0; + for (const FHyperTwistTrainingSessionTemplate& SessionTemplate : CachedActiveTrainingSessionTemplates) + { + if (HyperTwistCoachDashboardWidgetInternal::ScoreTemplateForDashboardLaunch(SessionTemplate) > 0) + { + EligibleTemplateLaunchCount += 1; + } + } + const bool bHasPreferredTemplateLaunch = PreferredTemplateLaunch != nullptr; + const bool bPreferredTemplateLaunchUsesReviewLane = + bHasPreferredTemplateLaunch + && (PreferredTemplateLaunch->bPreferReviewRun + || PreferredTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::ReviewPlan + || PreferredTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::Coach); + const FString PreferredTemplateLaunchTitle = bHasPreferredTemplateLaunch + ? (PreferredTemplateLaunch->Title.IsEmpty() + ? PreferredTemplateLaunch->TemplateId + : PreferredTemplateLaunch->Title) + : TEXT("n/a"); + const FString PreferredTemplateLaunchFocusPreview = bHasPreferredTemplateLaunch + ? BuildCaseIdPreview(PreferredTemplateLaunch->FocusCaseIds) + : TEXT("none"); + const bool bCanLaunchPreferredTemplate = + !bHasLiveAttemptTimer && !bHasActiveRun && bHasPreferredTemplateLaunch; + FString TemplateLaunchStatusLine; + if (!bHasPreferredTemplateLaunch) + { + TemplateLaunchStatusLine = FString::Printf( + TEXT("Template launch: no active review/recovery template is ready in the dashboard cache. Eligible %d of %d active templates."), + EligibleTemplateLaunchCount, + CachedActiveTrainingSessionTemplates.Num() + ); + } + else if (bHasLiveAttemptTimer) + { + TemplateLaunchStatusLine = FString::Printf( + TEXT("Template launch: %s is ready, but a live attempt is active."), + *PreferredTemplateLaunchTitle + ); + } + else if (bHasActiveRun) + { + TemplateLaunchStatusLine = FString::Printf( + TEXT("Template launch: %s is ready, but clear or complete the active run first."), + *PreferredTemplateLaunchTitle + ); + } + else + { + TemplateLaunchStatusLine = FString::Printf( + TEXT("Template launch: %s is ready on %s. Eligible %d of %d active templates."), + *PreferredTemplateLaunchTitle, + PreferredTemplateLaunch->FocusDeckId.IsEmpty() + ? TEXT("n/a") + : *PreferredTemplateLaunch->FocusDeckId, + EligibleTemplateLaunchCount, + CachedActiveTrainingSessionTemplates.Num() + ); + } + FString TemplateLaunchDetailLine; + if (!bHasPreferredTemplateLaunch) + { + TemplateLaunchDetailLine = + TEXT("Template launch detail: this dashboard only surfaces active review-plan, recovery, or coach-review templates from the current session-template cache."); + } + 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"), + *PreferredTemplateLaunchTitle, + PreferredTemplateLaunch->TemplateId.IsEmpty() + ? TEXT("n/a") + : *PreferredTemplateLaunch->TemplateId, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + PreferredTemplateLaunch->TemplateKind), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + PreferredTemplateLaunch->SuggestedMode), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + PreferredTemplateLaunch->SuggestedSelectionPolicy), + PreferredTemplateLaunch->RecommendedCaseCount, + *PreferredTemplateLaunchFocusPreview, + PreferredTemplateLaunch->GuidanceLabel.IsEmpty() + ? TEXT("n/a") + : *PreferredTemplateLaunch->GuidanceLabel, + PreferredTemplateLaunch->MethodSegmentId.IsEmpty() + ? TEXT("n/a") + : *PreferredTemplateLaunch->MethodSegmentId, + PreferredTemplateLaunch->UserId.IsEmpty() + ? TEXT("n/a") + : *PreferredTemplateLaunch->UserId, + PreferredTemplateLaunch->SourceReviewPlanId.IsEmpty() + ? TEXT("none") + : *PreferredTemplateLaunch->SourceReviewPlanId, + bPreferredTemplateLaunchUsesReviewLane ? TEXT("yes") : TEXT("no"), + PreferredTemplateLaunch->bPreferShortReviewSet ? TEXT("yes") : TEXT("no") + ); + } const bool bCanUseCoachHandoff = !bHasLiveAttemptTimer && !bHasActiveRun; FString CoachHandoffStatusLine; if (bHasLiveAttemptTimer) @@ -14888,6 +15131,18 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { CoachHandoffRecommendationDetailTextBlock->SetText(FText::FromString(CoachHandoffRecommendationDetailLine)); } + if (TemplateLaunchHeaderTextBlock != nullptr) + { + TemplateLaunchHeaderTextBlock->SetText(FText::FromString(TEXT("[Template Launch]"))); + } + if (TemplateLaunchStatusTextBlock != nullptr) + { + TemplateLaunchStatusTextBlock->SetText(FText::FromString(TemplateLaunchStatusLine)); + } + if (TemplateLaunchDetailTextBlock != nullptr) + { + TemplateLaunchDetailTextBlock->SetText(FText::FromString(TemplateLaunchDetailLine)); + } if (HandoffHistoryHeaderTextBlock != nullptr) { HandoffHistoryHeaderTextBlock->SetText(FText::FromString(TEXT("[Handoff History]"))); @@ -15027,6 +15282,32 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { StartRecommendedRunButton->SetIsEnabled(bCanUseCoachHandoff && bRecommendedReadyForHandoff); } + FString TemplateLaunchButtonLabel = TEXT("No Template"); + if (bHasPreferredTemplateLaunch) + { + TemplateLaunchButtonLabel = + PreferredTemplateLaunchTitle.Len() <= 28 + ? PreferredTemplateLaunchTitle + : (bPreferredTemplateLaunchUsesReviewLane + ? TEXT("Launch Review Template") + : TEXT("Launch Recovery Template")); + } + if (bHasLiveAttemptTimer && bHasPreferredTemplateLaunch) + { + TemplateLaunchButtonLabel = TEXT("Template Busy"); + } + else if (bHasActiveRun && bHasPreferredTemplateLaunch) + { + TemplateLaunchButtonLabel = TEXT("Clear Run For Template"); + } + if (StartTemplateLaunchButtonLabel != nullptr) + { + StartTemplateLaunchButtonLabel->SetText(FText::FromString(TemplateLaunchButtonLabel)); + } + if (StartTemplateLaunchButton != nullptr) + { + StartTemplateLaunchButton->SetIsEnabled(bCanLaunchPreferredTemplate); + } if (PreviewRecommendedButtonLabel != nullptr) { PreviewRecommendedButtonLabel->SetText(FText::FromString(TEXT("Preview Recommended"))); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 3789b42..13b3948 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -626,6 +626,9 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") FHyperTwistTrainingDeck PreviewFollowUpCoachDeck(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingRunState StartPreferredActiveSessionTemplateRun(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool SelectPreviousGuidancePreviewCase(); @@ -827,6 +830,15 @@ protected: UPROPERTY(Transient) TObjectPtr CoachHandoffRecommendationDetailTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr TemplateLaunchHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr TemplateLaunchStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr TemplateLaunchDetailTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr HandoffHistoryHeaderTextBlock = nullptr; @@ -938,6 +950,12 @@ protected: UPROPERTY(Transient) TObjectPtr StartRecommendedRunButtonLabel = nullptr; + UPROPERTY(Transient) + TObjectPtr StartTemplateLaunchButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr StartTemplateLaunchButtonLabel = nullptr; + UPROPERTY(Transient) TObjectPtr PreviewRecommendedButton = nullptr; @@ -1346,6 +1364,9 @@ protected: UFUNCTION() void HandleStartRecommendedRunClicked(); + UFUNCTION() + void HandleStartTemplateLaunchClicked(); + UFUNCTION() void HandlePreviewRecommendedClicked(); @@ -1491,6 +1512,7 @@ private: const FHyperTwistTrainingTimingPolicy* FindActiveTimingPolicy() const; const FHyperTwistTrainingDeck* GetActiveGuidancePreviewDeck() const; const FHyperTwistCoachBrief* GetActiveGuidancePreviewBrief() const; + const FHyperTwistTrainingSessionTemplate* FindPreferredDashboardTemplateLaunch() const; FString ResolveDashboardHandoffStartActionSourceLabel( EHyperTwistTrainingCoachHandoffKind HandoffKind) const; FHyperTwistTrainingDeck BuildSelectedGuidancePreviewCaseDeck() const; diff --git a/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_TEMPLATE_LAUNCH_AFFORDANCE_PACKET_2026-05-06.md b/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_TEMPLATE_LAUNCH_AFFORDANCE_PACKET_2026-05-06.md new file mode 100644 index 0000000..d0c8150 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_TEMPLATE_LAUNCH_AFFORDANCE_PACKET_2026-05-06.md @@ -0,0 +1,98 @@ +# HyperTwist Phase 4 dashboard template launch affordance packet + +Created on `2026-05-06` + +Status: + +- first-party HyperTwist packet +- bounded Phase `4` dashboard template launch slice + +## Purpose + +This packet exposes one explicit template-backed launch affordance in the first-party coach dashboard. + +The open tasks are: + +- surface the highest-priority active review/recovery template in the dashboard itself +- make the corrected review-template launch lane reachable from the first-party dashboard UX +- keep the affordance narrow by reusing existing panel template caches and runtime launch wrappers + +It is not: + +- a dashboard redesign packet +- a template browser or editor packet +- a broader coach handoff rewrite +- a new template derivation packet + +## Scope + +Bounded lane: + +- rank active session templates for dashboard launch priority +- prefer active review-plan, recovery, and coach-review templates +- add a dedicated dashboard template-launch status/detail section +- add one dashboard button that launches the preferred active template through `StartTrainingRunFromTemplate(...)` +- keep existing coach handoff, preview, and method-drill flows unchanged + +Out of scope: + +- adding multi-template pagination or selection UI +- changing template merge or derivation order in the repository/subsystem +- changing review-plan semantics +- changing coach queue or guidance decision logic + +## Why this was the right next packet + +The previous packet gave first-party panel and actor consumers parity with the runtime template APIs. + +That still left one practical gap: + +- the dashboard remained the main first-party operator surface +- but it still offered no direct session-template launch affordance +- so review-preferring templates were technically reachable yet still visually absent from the built-in dashboard workflow + +The next honest move was therefore: + +- keep template logic where it already lived +- rank the active template cache for one narrow review/recovery launch choice +- and surface that choice directly in the dashboard beside the existing handoff actions + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h` + - added a Blueprint-callable dashboard action for starting the preferred active template + - added dashboard text/button members for the new template-launch surface + - added a private helper for selecting the preferred dashboard template launch target +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp` + - added narrow dashboard launch scoring for active session templates + - added the new dashboard template-launch action and button handler + - added a `[Template Launch]` status/detail section + - added a direct template-launch button to the coach handoff action row + - updates now describe and enable the highest-priority active review/recovery template when available + +## Product effect + +The first-party dashboard now makes the corrected template lane visible: + +- operators can see whether an active review/recovery template is available +- the dashboard shows which active template won launch priority and why it is or is not launchable +- a direct dashboard button now launches that preferred template through the existing template-backed run path +- review-preferring templates now have a visible first-party dashboard entry point instead of living only behind lower-level calls + +## Acceptance criteria + +- dashboard surfaces a preferred active review/recovery template when one exists +- dashboard button launches that preferred template through `StartTrainingRunFromTemplate(...)` +- dashboard disables the template action while a live attempt or active run blocks launch +- full product build succeeds + +## Validation checklist + +1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor` +2. confirm the dashboard shows a `[Template Launch]` section +3. confirm the button label and enabled state track the preferred active template +4. confirm clicking the button launches the preferred active template-backed run + +That is the packet.