From 3d67ba1df7998e9aaf4c849d57175f5e781aca67 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 27 Apr 2026 04:38:25 +0200 Subject: [PATCH] Add authored coach dashboard widget --- .../HyperTwistCoachDashboardWidget.cpp | 367 ++++++++++++++++++ .../HyperTwistCoachDashboardWidget.h | 97 +++++ 2 files changed, 464 insertions(+) create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp new file mode 100644 index 0000000..ae78e78 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -0,0 +1,367 @@ +#include "HyperTwistTraining/HyperTwistCoachDashboardWidget.h" + +#include "Blueprint/WidgetTree.h" +#include "Components/Button.h" +#include "Components/ButtonSlot.h" +#include "Components/HorizontalBox.h" +#include "Components/HorizontalBoxSlot.h" +#include "Components/TextBlock.h" +#include "Components/VerticalBox.h" +#include "Components/VerticalBoxSlot.h" + +namespace HyperTwistCoachDashboardWidgetInternal +{ + UTextBlock* AddTextRow( + UWidgetTree* WidgetTree, + UVerticalBox* Parent, + const FName WidgetName, + const int32 PaddingBottom) + { + UTextBlock* TextBlock = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), WidgetName); + if (TextBlock == nullptr) + { + return nullptr; + } + + if (UVerticalBoxSlot* Slot = Parent->AddChildToVerticalBox(TextBlock)) + { + Slot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, static_cast(PaddingBottom))); + } + return TextBlock; + } + + UButton* AddButton( + UWidgetTree* WidgetTree, + UHorizontalBox* Parent, + const FName ButtonName, + const FName LabelName, + const FString& LabelText, + UTextBlock*& OutLabel) + { + UButton* Button = WidgetTree->ConstructWidget(UButton::StaticClass(), ButtonName); + if (Button == nullptr) + { + return nullptr; + } + + OutLabel = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), LabelName); + if (OutLabel != nullptr) + { + OutLabel->SetText(FText::FromString(LabelText)); + if (UButtonSlot* ButtonSlot = Cast(Button->AddChild(OutLabel))) + { + ButtonSlot->SetPadding(FMargin(8.0f, 4.0f)); + ButtonSlot->SetHorizontalAlignment(HAlign_Center); + ButtonSlot->SetVerticalAlignment(VAlign_Center); + } + } + + if (UHorizontalBoxSlot* Slot = Parent->AddChildToHorizontalBox(Button)) + { + Slot->SetPadding(FMargin(0.0f, 0.0f, 8.0f, 0.0f)); + } + + return Button; + } +} + +void UHyperTwistCoachDashboardWidget::NativeConstruct() +{ + EnsureDefaultDashboardBuilt(); + Super::NativeConstruct(); + RefreshCoachDashboardView(); +} + +void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView() +{ + RefreshTrainingState(); + UpdateDashboardPresentation(); +} + +FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::ExecutePrimaryCoachAction() +{ + FHyperTwistTrainingRunState RunState; + if (CachedCoachPanelState.bCanStartQueuedRun) + { + RunState = StartNextQueuedCoachRun(DefaultCoachMaxCases); + } + else if (CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance) + { + RunState = StartCoachFollowUpRun(DefaultCoachMaxCases); + } + else if (CachedCoachPanelState.bCanStartCoachRecommendedRun) + { + RunState = StartCoachRecommendedRun(DefaultCoachMaxCases); + } + + UpdateDashboardPresentation(); + return RunState; +} + +bool UHyperTwistCoachDashboardWidget::DeferDisplayedQueueEntryByDefaultWindow() +{ + const bool bSucceeded = DeferDisplayedCoachQueueEntry( + BuildDefaultDeferredUntilUtc(), + TEXT("dashboard-defer"), + true + ); + UpdateDashboardPresentation(); + return bSucceeded; +} + +bool UHyperTwistCoachDashboardWidget::PromoteFirstDeferredQueueEntry() +{ + const FString DeferredEntryId = FindFirstDeferredQueueEntryId(); + if (DeferredEntryId.IsEmpty()) + { + return false; + } + + const bool bSucceeded = PromoteCoachQueueEntry(DeferredEntryId); + UpdateDashboardPresentation(); + return bSucceeded; +} + +void UHyperTwistCoachDashboardWidget::HandlePrimaryActionClicked() +{ + ExecutePrimaryCoachAction(); +} + +void UHyperTwistCoachDashboardWidget::HandleVerifyClicked() +{ + VerifyTrainingRepositoryRoundTrip(); + UpdateDashboardPresentation(); +} + +void UHyperTwistCoachDashboardWidget::HandleDeferClicked() +{ + DeferDisplayedQueueEntryByDefaultWindow(); +} + +void UHyperTwistCoachDashboardWidget::HandlePromoteClicked() +{ + PromoteFirstDeferredQueueEntry(); +} + +void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() +{ + if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr) + { + return; + } + + RootLayout = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("CoachDashboardRoot")); + if (RootLayout == nullptr) + { + return; + } + + WidgetTree->RootWidget = RootLayout; + + HeadlineTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachHeadline"), + 6 + ); + SecondaryTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachSecondary"), + 4 + ); + FocusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachFocus"), + 8 + ); + QueueStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachQueueStatus"), + 2 + ); + QueueCountsTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachQueueCounts"), + 2 + ); + VerificationStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachVerificationStatus"), + 10 + ); + + UHorizontalBox* ButtonRow = WidgetTree->ConstructWidget( + UHorizontalBox::StaticClass(), + TEXT("CoachDashboardButtons") + ); + if (ButtonRow != nullptr) + { + if (UVerticalBoxSlot* ButtonRowSlot = RootLayout->AddChildToVerticalBox(ButtonRow)) + { + ButtonRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)); + } + + UTextBlock* PrimaryActionLabelRaw = nullptr; + PrimaryActionButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + ButtonRow, + TEXT("PrimaryActionButton"), + TEXT("PrimaryActionButtonLabel"), + TEXT("Start"), + PrimaryActionLabelRaw + ); + PrimaryActionButtonLabel = PrimaryActionLabelRaw; + + UTextBlock* VerifyLabelRaw = nullptr; + VerifyButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + ButtonRow, + TEXT("VerifyButton"), + TEXT("VerifyButtonLabel"), + TEXT("Verify"), + VerifyLabelRaw + ); + VerifyButtonLabel = VerifyLabelRaw; + + UTextBlock* DeferLabelRaw = nullptr; + DeferButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + ButtonRow, + TEXT("DeferButton"), + TEXT("DeferButtonLabel"), + TEXT("Defer"), + DeferLabelRaw + ); + DeferButtonLabel = DeferLabelRaw; + + UTextBlock* PromoteLabelRaw = nullptr; + PromoteButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + ButtonRow, + TEXT("PromoteButton"), + TEXT("PromoteButtonLabel"), + TEXT("Promote"), + PromoteLabelRaw + ); + PromoteButtonLabel = PromoteLabelRaw; + } + + if (PrimaryActionButton != nullptr) + { + PrimaryActionButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePrimaryActionClicked); + } + if (VerifyButton != nullptr) + { + VerifyButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleVerifyClicked); + } + if (DeferButton != nullptr) + { + DeferButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleDeferClicked); + } + if (PromoteButton != nullptr) + { + PromoteButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePromoteClicked); + } +} + +void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() +{ + if (HeadlineTextBlock != nullptr) + { + HeadlineTextBlock->SetText(FText::FromString(CachedCoachPanelState.Headline)); + } + if (SecondaryTextBlock != nullptr) + { + SecondaryTextBlock->SetText(FText::FromString(CachedCoachPanelState.SecondaryLine)); + } + if (FocusTextBlock != nullptr) + { + FocusTextBlock->SetText(FText::FromString(FString::Printf( + TEXT("Focus deck: %s | Method: %s | Cases: %d"), + CachedCoachPanelState.FocusDeckId.IsEmpty() ? TEXT("n/a") : *CachedCoachPanelState.FocusDeckId, + CachedCoachPanelState.FocusMethodSegmentId.IsEmpty() ? TEXT("n/a") : *CachedCoachPanelState.FocusMethodSegmentId, + CachedCoachPanelState.FocusCaseCount + ))); + } + if (QueueStatusTextBlock != nullptr) + { + QueueStatusTextBlock->SetText(FText::FromString(CachedCoachPanelState.QueueStatusLine)); + } + if (QueueCountsTextBlock != nullptr) + { + QueueCountsTextBlock->SetText(FText::FromString(FString::Printf( + TEXT("Signals: %d | Follow-up queue entries: %d | Runnable: %s"), + CachedCoachPanelState.SignalCount, + CachedCoachPanelState.FollowUpQueueEntryCount, + CachedCoachPanelState.bHasRunnableQueueEntry ? TEXT("yes") : TEXT("no") + ))); + } + if (VerificationStatusTextBlock != nullptr) + { + VerificationStatusTextBlock->SetText(FText::FromString(CachedCoachPanelState.VerificationStatusLine)); + } + + FString PrimaryLabel = CachedCoachPanelState.PrimaryActionLabel; + if (PrimaryLabel.IsEmpty()) + { + PrimaryLabel = TEXT("Refresh"); + } + if (PrimaryActionButtonLabel != nullptr) + { + PrimaryActionButtonLabel->SetText(FText::FromString(PrimaryLabel)); + } + if (PrimaryActionButton != nullptr) + { + PrimaryActionButton->SetIsEnabled( + CachedCoachPanelState.bCanStartQueuedRun + || CachedCoachPanelState.bCanStartCoachFollowUpRun + || CachedCoachPanelState.bCanStartCoachRecommendedRun + ); + } + + if (VerifyButtonLabel != nullptr) + { + VerifyButtonLabel->SetText(FText::FromString(TEXT("Verify"))); + } + if (DeferButtonLabel != nullptr) + { + DeferButtonLabel->SetText(FText::FromString(TEXT("Defer 24h"))); + } + if (DeferButton != nullptr) + { + DeferButton->SetIsEnabled(CachedCoachPanelState.bHasRunnableQueueEntry); + } + if (PromoteButtonLabel != nullptr) + { + PromoteButtonLabel->SetText(FText::FromString(TEXT("Promote Deferred"))); + } + if (PromoteButton != nullptr) + { + PromoteButton->SetIsEnabled(!FindFirstDeferredQueueEntryId().IsEmpty()); + } +} + +FString UHyperTwistCoachDashboardWidget::BuildDefaultDeferredUntilUtc() const +{ + const FDateTime DeferredTimeUtc = + FDateTime::UtcNow() + FTimespan::FromHours(FMath::Max(DefaultDeferredHours, 1)); + return DeferredTimeUtc.ToIso8601(); +} + +FString UHyperTwistCoachDashboardWidget::FindFirstDeferredQueueEntryId() const +{ + for (const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : CachedCoachSessionQueueState.Entries) + { + if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred) + { + return Entry.EntryId; + } + } + + return FString(); +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h new file mode 100644 index 0000000..49d6462 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -0,0 +1,97 @@ +#pragma once + +#include "CoreMinimal.h" +#include "HyperTwistTraining/HyperTwistTrainingPanelWidget.h" +#include "HyperTwistCoachDashboardWidget.generated.h" + +class UButton; +class UTextBlock; +class UVerticalBox; + +UCLASS(BlueprintType, Blueprintable) +class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget +{ + GENERATED_BODY() + +public: + virtual void NativeConstruct() override; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard") + int32 DefaultDeferredHours = 24; + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + void RefreshCoachDashboardView(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingRunState ExecutePrimaryCoachAction(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool DeferDisplayedQueueEntryByDefaultWindow(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool PromoteFirstDeferredQueueEntry(); + +protected: + UPROPERTY(Transient) + TObjectPtr RootLayout = nullptr; + + UPROPERTY(Transient) + TObjectPtr HeadlineTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr SecondaryTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr FocusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr QueueStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr VerificationStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr QueueCountsTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr PrimaryActionButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr PrimaryActionButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr VerifyButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr VerifyButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr DeferButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr DeferButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr PromoteButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr PromoteButtonLabel = nullptr; + + UFUNCTION() + void HandlePrimaryActionClicked(); + + UFUNCTION() + void HandleVerifyClicked(); + + UFUNCTION() + void HandleDeferClicked(); + + UFUNCTION() + void HandlePromoteClicked(); + +private: + void EnsureDefaultDashboardBuilt(); + void UpdateDashboardPresentation(); + FString BuildDefaultDeferredUntilUtc() const; + FString FindFirstDeferredQueueEntryId() const; +};