From 93cc528fcd788dc89ffabcdff9bebf24e950d019 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 27 Apr 2026 15:14:32 +0200 Subject: [PATCH] Add guidance case review to coach dashboard --- .../HyperTwistCoachDashboardWidget.cpp | 195 ++++++++++++++++++ .../HyperTwistCoachDashboardWidget.h | 42 ++++ 2 files changed, 237 insertions(+) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 9452c8d..64b1819 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -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::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) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 06c8fd8..b28b1d7 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -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 GuidanceReviewHeaderTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr GuidancePreviewSelectionTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr RecommendedDeckPreviewTextBlock = nullptr; UPROPERTY(Transient) TObjectPtr FollowUpDeckPreviewTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr GuidancePreviewCaseDetailTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr RepeatRunButton = nullptr; @@ -226,6 +245,18 @@ protected: UPROPERTY(Transient) TObjectPtr PreviewFollowUpButtonLabel = nullptr; + UPROPERTY(Transient) + TObjectPtr PreviousGuidanceCaseButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousGuidanceCaseButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextGuidanceCaseButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextGuidanceCaseButtonLabel = nullptr; + UPROPERTY(Transient) TObjectPtr 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; };