Add guidance case review to coach dashboard

This commit is contained in:
axiomlogicnexus 2026-04-27 15:14:32 +02:00
parent 12c675d211
commit 93cc528fcd
2 changed files with 237 additions and 0 deletions

View file

@ -306,6 +306,10 @@ bool UHyperTwistCoachDashboardWidget::SelectNextAttemptReview()
FHyperTwistTrainingDeck UHyperTwistCoachDashboardWidget::PreviewRecommendedCoachDeck()
{
const FHyperTwistTrainingDeck Deck = BuildActiveCoachRecommendedDeck(DefaultCoachMaxCases);
ActiveGuidancePreviewLane = Deck.IsStructurallyValid()
? EHyperTwistCoachDashboardGuidancePreviewLane::Recommended
: EHyperTwistCoachDashboardGuidancePreviewLane::None;
SyncSelectedGuidancePreviewCase();
UpdateDashboardPresentation();
return Deck;
}
@ -313,10 +317,56 @@ FHyperTwistTrainingDeck UHyperTwistCoachDashboardWidget::PreviewRecommendedCoach
FHyperTwistTrainingDeck UHyperTwistCoachDashboardWidget::PreviewFollowUpCoachDeck()
{
const FHyperTwistTrainingDeck Deck = BuildActiveCoachFollowUpDeck(DefaultCoachMaxCases);
ActiveGuidancePreviewLane = Deck.IsStructurallyValid()
? EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp
: EHyperTwistCoachDashboardGuidancePreviewLane::None;
SyncSelectedGuidancePreviewCase();
UpdateDashboardPresentation();
return Deck;
}
bool UHyperTwistCoachDashboardWidget::SelectPreviousGuidancePreviewCase()
{
const FHyperTwistTrainingDeck* Deck = GetActiveGuidancePreviewDeck();
if (Deck == nullptr || Deck->Cases.Num() <= 0)
{
SelectedGuidancePreviewCaseIndex = INDEX_NONE;
return false;
}
if (SelectedGuidancePreviewCaseIndex == INDEX_NONE)
{
SyncSelectedGuidancePreviewCase();
return SelectedGuidancePreviewCaseIndex != INDEX_NONE;
}
SelectedGuidancePreviewCaseIndex =
(SelectedGuidancePreviewCaseIndex - 1 + Deck->Cases.Num()) % Deck->Cases.Num();
UpdateDashboardPresentation();
return true;
}
bool UHyperTwistCoachDashboardWidget::SelectNextGuidancePreviewCase()
{
const FHyperTwistTrainingDeck* Deck = GetActiveGuidancePreviewDeck();
if (Deck == nullptr || Deck->Cases.Num() <= 0)
{
SelectedGuidancePreviewCaseIndex = INDEX_NONE;
return false;
}
if (SelectedGuidancePreviewCaseIndex == INDEX_NONE)
{
SyncSelectedGuidancePreviewCase();
return SelectedGuidancePreviewCaseIndex != INDEX_NONE;
}
SelectedGuidancePreviewCaseIndex =
(SelectedGuidancePreviewCaseIndex + 1) % Deck->Cases.Num();
UpdateDashboardPresentation();
return true;
}
FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::RestartRetainedRunRecapDeck()
{
if (!RetainedRunRecapDeck.IsStructurallyValid())
@ -654,6 +704,16 @@ void UHyperTwistCoachDashboardWidget::HandlePreviewFollowUpClicked()
PreviewFollowUpCoachDeck();
}
void UHyperTwistCoachDashboardWidget::HandlePreviousGuidanceCaseClicked()
{
SelectPreviousGuidancePreviewCase();
}
void UHyperTwistCoachDashboardWidget::HandleNextGuidanceCaseClicked()
{
SelectNextGuidancePreviewCase();
}
void UHyperTwistCoachDashboardWidget::HandleCompleteRunClicked()
{
CompleteActiveTrainingRun(false);
@ -903,6 +963,12 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
TEXT("CoachGuidanceReviewHeader"),
2
);
GuidancePreviewSelectionTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachGuidancePreviewSelection"),
2
);
RecommendedDeckPreviewTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
@ -915,6 +981,12 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
TEXT("CoachFollowUpDeckPreview"),
2
);
GuidancePreviewCaseDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachGuidancePreviewCaseDetail"),
2
);
UHorizontalBox* RunRecapActionRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("CoachDashboardRunRecapActions")
@ -1024,6 +1096,28 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
PreviewFollowUpLabelRaw
);
PreviewFollowUpButtonLabel = PreviewFollowUpLabelRaw;
UTextBlock* PreviousGuidanceCaseLabelRaw = nullptr;
PreviousGuidanceCaseButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
GuidancePreviewActionRow,
TEXT("PreviousGuidanceCaseButton"),
TEXT("PreviousGuidanceCaseButtonLabel"),
TEXT("Prev Guidance Case"),
PreviousGuidanceCaseLabelRaw
);
PreviousGuidanceCaseButtonLabel = PreviousGuidanceCaseLabelRaw;
UTextBlock* NextGuidanceCaseLabelRaw = nullptr;
NextGuidanceCaseButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
GuidancePreviewActionRow,
TEXT("NextGuidanceCaseButton"),
TEXT("NextGuidanceCaseButtonLabel"),
TEXT("Next Guidance Case"),
NextGuidanceCaseLabelRaw
);
NextGuidanceCaseButtonLabel = NextGuidanceCaseLabelRaw;
}
LastAttemptTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
@ -1562,6 +1656,14 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
{
PreviewFollowUpButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviewFollowUpClicked);
}
if (PreviousGuidanceCaseButton != nullptr)
{
PreviousGuidanceCaseButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviousGuidanceCaseClicked);
}
if (NextGuidanceCaseButton != nullptr)
{
NextGuidanceCaseButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleNextGuidanceCaseClicked);
}
if (CompleteRunButton != nullptr)
{
CompleteRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleCompleteRunClicked);
@ -1730,6 +1832,35 @@ void UHyperTwistCoachDashboardWidget::SyncRetainedRunRecap()
}
}
void UHyperTwistCoachDashboardWidget::SyncSelectedGuidancePreviewCase()
{
const FHyperTwistTrainingDeck* Deck = GetActiveGuidancePreviewDeck();
if (Deck == nullptr || Deck->Cases.Num() <= 0)
{
SelectedGuidancePreviewCaseIndex = INDEX_NONE;
return;
}
if (SelectedGuidancePreviewCaseIndex < 0
|| SelectedGuidancePreviewCaseIndex >= Deck->Cases.Num())
{
SelectedGuidancePreviewCaseIndex = 0;
}
}
const FHyperTwistTrainingDeck* UHyperTwistCoachDashboardWidget::GetActiveGuidancePreviewDeck() const
{
switch (ActiveGuidancePreviewLane)
{
case EHyperTwistCoachDashboardGuidancePreviewLane::Recommended:
return CachedCoachRecommendedDeck.IsStructurallyValid() ? &CachedCoachRecommendedDeck : nullptr;
case EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp:
return CachedCoachFollowUpDeck.IsStructurallyValid() ? &CachedCoachFollowUpDeck : nullptr;
default:
return nullptr;
}
}
void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid()
@ -2053,6 +2184,42 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
CachedCoachFollowUpBrief,
TEXT("Follow-up guidance")
);
const FHyperTwistTrainingDeck* ActivePreviewDeck = GetActiveGuidancePreviewDeck();
const FHyperTwistTrainingCase* SelectedGuidancePreviewCase =
ActivePreviewDeck != nullptr
&& SelectedGuidancePreviewCaseIndex >= 0
&& SelectedGuidancePreviewCaseIndex < ActivePreviewDeck->Cases.Num()
? &ActivePreviewDeck->Cases[SelectedGuidancePreviewCaseIndex]
: nullptr;
const FString ActivePreviewLaneLabel =
ActiveGuidancePreviewLane == EHyperTwistCoachDashboardGuidancePreviewLane::Recommended
? TEXT("recommended")
: (ActiveGuidancePreviewLane == EHyperTwistCoachDashboardGuidancePreviewLane::FollowUp
? TEXT("follow-up")
: TEXT("none"));
const FString GuidancePreviewSelectionLine =
SelectedGuidancePreviewCase != nullptr && ActivePreviewDeck != nullptr
? FString::Printf(
TEXT("Active preview: %s | case %d/%d | deck %s"),
*ActivePreviewLaneLabel,
SelectedGuidancePreviewCaseIndex + 1,
ActivePreviewDeck->Cases.Num(),
ActivePreviewDeck->Title.IsEmpty() ? *ActivePreviewDeck->DeckId : *ActivePreviewDeck->Title
)
: FString::Printf(TEXT("Active preview: %s | build a coach deck preview to inspect specific cases"), *ActivePreviewLaneLabel);
const FString GuidancePreviewCaseDetailLine =
SelectedGuidancePreviewCase != nullptr
? FString::Printf(
TEXT("Preview case: %s | Prompt: %s | Kind: %s | Subset: %s | Target: %d ms | Scramble: %s | Canonical: %s"),
SelectedGuidancePreviewCase->CaseId.IsEmpty() ? TEXT("n/a") : *SelectedGuidancePreviewCase->CaseId,
SelectedGuidancePreviewCase->PromptLabel.IsEmpty() ? TEXT("n/a") : *SelectedGuidancePreviewCase->PromptLabel,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(SelectedGuidancePreviewCase->PromptKind),
SelectedGuidancePreviewCase->SubsetId.IsEmpty() ? TEXT("n/a") : *SelectedGuidancePreviewCase->SubsetId,
SelectedGuidancePreviewCase->TimeTargetMs,
SelectedGuidancePreviewCase->ScrambleNotation.IsEmpty() ? TEXT("n/a") : *SelectedGuidancePreviewCase->ScrambleNotation,
SelectedGuidancePreviewCase->CanonicalNotation.IsEmpty() ? TEXT("n/a") : *SelectedGuidancePreviewCase->CanonicalNotation
)
: TEXT("Preview case: none selected yet.");
if (HeadlineTextBlock != nullptr)
{
@ -2194,6 +2361,10 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
GuidanceReviewHeaderTextBlock->SetText(FText::FromString(TEXT("[Guidance Review]")));
}
if (GuidancePreviewSelectionTextBlock != nullptr)
{
GuidancePreviewSelectionTextBlock->SetText(FText::FromString(GuidancePreviewSelectionLine));
}
if (RecommendedDeckPreviewTextBlock != nullptr)
{
RecommendedDeckPreviewTextBlock->SetText(FText::FromString(RecommendedDeckPreviewLine));
@ -2202,6 +2373,10 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
FollowUpDeckPreviewTextBlock->SetText(FText::FromString(FollowUpDeckPreviewLine));
}
if (GuidancePreviewCaseDetailTextBlock != nullptr)
{
GuidancePreviewCaseDetailTextBlock->SetText(FText::FromString(GuidancePreviewCaseDetailLine));
}
if (StartQueuedRunButtonLabel != nullptr)
{
StartQueuedRunButtonLabel->SetText(FText::FromString(QueuedRunLabel));
@ -2254,6 +2429,26 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
&& CachedCoachPanelState.bHasFollowUpGuidance
);
}
if (PreviousGuidanceCaseButtonLabel != nullptr)
{
PreviousGuidanceCaseButtonLabel->SetText(FText::FromString(TEXT("Prev Guidance Case")));
}
if (PreviousGuidanceCaseButton != nullptr)
{
PreviousGuidanceCaseButton->SetIsEnabled(
ActivePreviewDeck != nullptr && ActivePreviewDeck->Cases.Num() > 1
);
}
if (NextGuidanceCaseButtonLabel != nullptr)
{
NextGuidanceCaseButtonLabel->SetText(FText::FromString(TEXT("Next Guidance Case")));
}
if (NextGuidanceCaseButton != nullptr)
{
NextGuidanceCaseButton->SetIsEnabled(
ActivePreviewDeck != nullptr && ActivePreviewDeck->Cases.Num() > 1
);
}
if (LastAttemptTextBlock != nullptr)
{
if (ReviewedAttempt != nullptr)

View file

@ -16,6 +16,13 @@ enum class EHyperTwistCoachDashboardAttemptPhase : uint8
Solving
};
enum class EHyperTwistCoachDashboardGuidancePreviewLane : uint8
{
None,
Recommended,
FollowUp
};
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget
{
@ -103,6 +110,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
FHyperTwistTrainingDeck PreviewFollowUpCoachDeck();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool SelectPreviousGuidancePreviewCase();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool SelectNextGuidancePreviewCase();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
FHyperTwistTrainingRunState RestartRetainedRunRecapDeck();
@ -178,12 +191,18 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidanceReviewHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidancePreviewSelectionTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> RecommendedDeckPreviewTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> FollowUpDeckPreviewTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidancePreviewCaseDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> RepeatRunButton = nullptr;
@ -226,6 +245,18 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PreviewFollowUpButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousGuidanceCaseButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PreviousGuidanceCaseButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> NextGuidanceCaseButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> NextGuidanceCaseButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LastAttemptTextBlock = nullptr;
@ -472,6 +503,12 @@ protected:
UFUNCTION()
void HandlePreviewFollowUpClicked();
UFUNCTION()
void HandlePreviousGuidanceCaseClicked();
UFUNCTION()
void HandleNextGuidanceCaseClicked();
UFUNCTION()
void HandleCompleteRunClicked();
@ -531,6 +568,7 @@ private:
void SyncSelectedQueueEntry();
void SyncSelectedAttempt();
void SyncRetainedRunRecap();
void SyncSelectedGuidancePreviewCase();
void UpdateDashboardPresentation();
void RefreshLiveAttemptClock();
FString BuildDefaultDeferredUntilUtc() const;
@ -540,6 +578,7 @@ private:
void SetAttemptTimeText(int32 TimeMs);
const FHyperTwistTrainingCoachSessionQueueStateEntry* FindSelectedQueueEntry() const;
const FHyperTwistTrainingTimingPolicy* FindActiveTimingPolicy() const;
const FHyperTwistTrainingDeck* GetActiveGuidancePreviewDeck() const;
bool IsLiveAttemptInspectionPhase() const;
int32 GetMaxManualSplitCaptureCount() const;
const FHyperTwistTrainingSplitPhaseDefinition* FindNextSplitPhaseToCapture() const;
@ -555,4 +594,7 @@ private:
FString RetainedRunRecapUserId;
FString RetainedRunRecapSessionId;
EHyperTwistTrainingDeliveryMode RetainedRunRecapMode = EHyperTwistTrainingDeliveryMode::Timer;
EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidancePreviewLane =
EHyperTwistCoachDashboardGuidancePreviewLane::None;
int32 SelectedGuidancePreviewCaseIndex = INDEX_NONE;
};