Expose dashboard template launch affordance

This commit is contained in:
axiomlogicnexus 2026-05-06 23:04:07 +02:00
parent 84b426443a
commit 5be3abea59
3 changed files with 401 additions and 0 deletions

View file

@ -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>(
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<int32>::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")));

View file

@ -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<UTextBlock> CoachHandoffRecommendationDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TemplateLaunchHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TemplateLaunchStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TemplateLaunchDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> HandoffHistoryHeaderTextBlock = nullptr;
@ -938,6 +950,12 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> StartRecommendedRunButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> StartTemplateLaunchButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> StartTemplateLaunchButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> 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;

View file

@ -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.