Add authored coach dashboard widget

This commit is contained in:
axiomlogicnexus 2026-04-27 04:38:25 +02:00
parent 2f70bdfc4f
commit 3d67ba1df7
2 changed files with 464 additions and 0 deletions

View file

@ -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>(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<float>(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>(UButton::StaticClass(), ButtonName);
if (Button == nullptr)
{
return nullptr;
}
OutLabel = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), LabelName);
if (OutLabel != nullptr)
{
OutLabel->SetText(FText::FromString(LabelText));
if (UButtonSlot* ButtonSlot = Cast<UButtonSlot>(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>(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>(
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();
}

View file

@ -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<UVerticalBox> RootLayout = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> HeadlineTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> SecondaryTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> FocusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> VerificationStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueCountsTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PrimaryActionButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PrimaryActionButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> VerifyButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> VerifyButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> DeferButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> DeferButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PromoteButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> 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;
};