Add coach dashboard attempt composer

This commit is contained in:
axiomlogicnexus 2026-04-27 05:35:55 +02:00
parent ac6362a03c
commit baccddc0a8
2 changed files with 377 additions and 2 deletions

View file

@ -3,6 +3,7 @@
#include "Blueprint/WidgetTree.h"
#include "Components/Button.h"
#include "Components/ButtonSlot.h"
#include "Components/EditableTextBox.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/TextBlock.h"
@ -75,6 +76,28 @@ namespace HyperTwistCoachDashboardWidgetInternal
return Button;
}
UEditableTextBox* AddEditableTextBox(
UWidgetTree* WidgetTree,
UHorizontalBox* Parent,
const FName WidgetName)
{
UEditableTextBox* TextBox = WidgetTree->ConstructWidget<UEditableTextBox>(
UEditableTextBox::StaticClass(),
WidgetName
);
if (TextBox == nullptr)
{
return nullptr;
}
if (UHorizontalBoxSlot* Slot = Parent->AddChildToHorizontalBox(TextBox))
{
Slot->SetPadding(FMargin(0.0f, 0.0f, 8.0f, 0.0f));
Slot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
return TextBox;
}
}
void UHyperTwistCoachDashboardWidget::NativeConstruct()
@ -226,6 +249,15 @@ FHyperTwistTrainingRunStepResult UHyperTwistCoachDashboardWidget::SubmitSyntheti
Attempt.ExecutionTimeMs = FMath::Max(TimeMs, 0);
Attempt.TimingBreakdown.RawSolveTimeMs = FMath::Max(TimeMs, 0);
Attempt.TimingBreakdown.FinalTimeMs = FMath::Max(TimeMs, 0);
Attempt.TimingBreakdown.bDidNotFinish =
Result == EHyperTwistTrainingAttemptResult::Timeout
|| Result == EHyperTwistTrainingAttemptResult::DNF
|| Result == EHyperTwistTrainingAttemptResult::Aborted;
if (Result == EHyperTwistTrainingAttemptResult::DNF)
{
Attempt.TimingBreakdown.ManualPenalty = EHyperTwistTrainingPenalty::DNF;
Attempt.TimingBreakdown.AppliedPenalty = EHyperTwistTrainingPenalty::DNF;
}
Attempt.ReplayId = CachedRunState.ReplayPacket.ReplayId;
Attempt.CompletedAtUtc = FDateTime::UtcNow().ToIso8601();
@ -283,14 +315,59 @@ void UHyperTwistCoachDashboardWidget::HandleClearRunClicked()
void UHyperTwistCoachDashboardWidget::HandleRecordSuccessClicked()
{
SubmitSyntheticCurrentAttempt(EHyperTwistTrainingAttemptResult::Success, DefaultSuccessAttemptTimeMs);
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::Success,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Success)
);
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
void UHyperTwistCoachDashboardWidget::HandleRecordFailureClicked()
{
SubmitSyntheticCurrentAttempt(EHyperTwistTrainingAttemptResult::Failure, DefaultFailureAttemptTimeMs);
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::Failure,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Failure)
);
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
void UHyperTwistCoachDashboardWidget::HandleRecordTimeoutClicked()
{
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::Timeout,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Timeout)
);
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
void UHyperTwistCoachDashboardWidget::HandleRecordDnfClicked()
{
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::DNF,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::DNF)
);
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
void UHyperTwistCoachDashboardWidget::HandleUseTargetTimeClicked()
{
if (CachedRunState.CurrentSelection.TrainingCase.TimeTargetMs > 0)
{
SetAttemptTimeText(CachedRunState.CurrentSelection.TrainingCase.TimeTargetMs);
}
UpdateDashboardPresentation();
}
void UHyperTwistCoachDashboardWidget::HandleUseAverageTimeClicked()
{
if (CachedRunSummary.AverageTotalTimeMs > 0)
{
SetAttemptTimeText(CachedRunSummary.AverageTotalTimeMs);
}
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
@ -383,6 +460,18 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
WidgetTree,
RootLayout,
TEXT("CoachCurrentCaseNotation"),
2
);
AttemptControlHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachAttemptControlHeader"),
2
);
AttemptInputStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachAttemptInputStatus"),
8
);
FollowUpTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
@ -514,6 +603,50 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
PromoteButtonLabel = PromoteLabelRaw;
}
UHorizontalBox* AttemptInputRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("CoachDashboardAttemptInputRow")
);
if (AttemptInputRow != nullptr)
{
if (UVerticalBoxSlot* AttemptInputRowSlot = RootLayout->AddChildToVerticalBox(AttemptInputRow))
{
AttemptInputRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f));
}
AttemptTimeTextBox = HyperTwistCoachDashboardWidgetInternal::AddEditableTextBox(
WidgetTree,
AttemptInputRow,
TEXT("AttemptTimeTextBox")
);
if (AttemptTimeTextBox != nullptr)
{
AttemptTimeTextBox->SetHintText(FText::FromString(TEXT("Enter attempt ms")));
}
UTextBlock* UseTargetTimeLabelRaw = nullptr;
UseTargetTimeButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
AttemptInputRow,
TEXT("UseTargetTimeButton"),
TEXT("UseTargetTimeButtonLabel"),
TEXT("Use Target"),
UseTargetTimeLabelRaw
);
UseTargetTimeButtonLabel = UseTargetTimeLabelRaw;
UTextBlock* UseAverageTimeLabelRaw = nullptr;
UseAverageTimeButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
AttemptInputRow,
TEXT("UseAverageTimeButton"),
TEXT("UseAverageTimeButtonLabel"),
TEXT("Use Avg"),
UseAverageTimeLabelRaw
);
UseAverageTimeButtonLabel = UseAverageTimeLabelRaw;
}
UHorizontalBox* RunControlRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("CoachDashboardRunControlButtons")
@ -568,6 +701,28 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
RecordFailureLabelRaw
);
RecordFailureButtonLabel = RecordFailureLabelRaw;
UTextBlock* RecordTimeoutLabelRaw = nullptr;
RecordTimeoutButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
RunControlRow,
TEXT("RecordTimeoutButton"),
TEXT("RecordTimeoutButtonLabel"),
TEXT("Record Timeout"),
RecordTimeoutLabelRaw
);
RecordTimeoutButtonLabel = RecordTimeoutLabelRaw;
UTextBlock* RecordDnfLabelRaw = nullptr;
RecordDnfButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
RunControlRow,
TEXT("RecordDnfButton"),
TEXT("RecordDnfButtonLabel"),
TEXT("Record DNF"),
RecordDnfLabelRaw
);
RecordDnfButtonLabel = RecordDnfLabelRaw;
}
UHorizontalBox* QueueNavRow = WidgetTree->ConstructWidget<UHorizontalBox>(
@ -647,6 +802,22 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
{
RecordFailureButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleRecordFailureClicked);
}
if (RecordTimeoutButton != nullptr)
{
RecordTimeoutButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleRecordTimeoutClicked);
}
if (RecordDnfButton != nullptr)
{
RecordDnfButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleRecordDnfClicked);
}
if (UseTargetTimeButton != nullptr)
{
UseTargetTimeButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleUseTargetTimeClicked);
}
if (UseAverageTimeButton != nullptr)
{
UseAverageTimeButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleUseAverageTimeClicked);
}
if (PreviousQueueButton != nullptr)
{
PreviousQueueButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked);
@ -727,6 +898,12 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
? (CachedRunState.ActiveDeck.DeckId.IsEmpty() ? TEXT("n/a") : CachedRunState.ActiveDeck.DeckId)
: CachedRunState.ActiveDeck.Title;
const bool bHasRunnableCurrentCase = bHasActiveRun && CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid();
int32 ParsedAttemptOverrideMs = 0;
const bool bHasParsedAttemptOverride = TryParseAttemptTimeOverride(ParsedAttemptOverrideMs);
const int32 ResolvedSuccessTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Success);
const int32 ResolvedFailureTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Failure);
const int32 ResolvedTimeoutTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Timeout);
const int32 ResolvedDnfTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::DNF);
const FString VerificationNotePreview = CachedRepositoryRoundTripVerification.VerificationNotes.Num() > 0
? CachedRepositoryRoundTripVerification.VerificationNotes[0]
: TEXT("none");
@ -848,6 +1025,31 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
));
}
}
if (AttemptControlHeaderTextBlock != nullptr)
{
AttemptControlHeaderTextBlock->SetText(FText::FromString(TEXT("[Attempt Controls]")));
}
if (AttemptInputStatusTextBlock != nullptr)
{
AttemptInputStatusTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Attempt ms input: %s | Target %d | Avg %d | Success %d | Failure %d | Timeout %d | DNF %d"),
bHasParsedAttemptOverride ? *FString::FromInt(ParsedAttemptOverrideMs) : TEXT("auto"),
CurrentTrainingCase.TimeTargetMs,
CachedRunSummary.AverageTotalTimeMs,
ResolvedSuccessTimeMs,
ResolvedFailureTimeMs,
ResolvedTimeoutTimeMs,
ResolvedDnfTimeMs
)));
}
if (AttemptTimeTextBox != nullptr)
{
const FString HintLabel = CurrentTrainingCase.TimeTargetMs > 0
? FString::Printf(TEXT("Enter attempt ms (target %d)"), CurrentTrainingCase.TimeTargetMs)
: TEXT("Enter attempt ms");
AttemptTimeTextBox->SetHintText(FText::FromString(HintLabel));
AttemptTimeTextBox->SetIsEnabled(bHasRunnableCurrentCase);
}
if (FollowUpTextBlock != nullptr)
{
const FString FollowUpLine =
@ -1093,6 +1295,38 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
RecordFailureButton->SetIsEnabled(bHasRunnableCurrentCase);
}
if (RecordTimeoutButtonLabel != nullptr)
{
RecordTimeoutButtonLabel->SetText(FText::FromString(TEXT("Record Timeout")));
}
if (RecordTimeoutButton != nullptr)
{
RecordTimeoutButton->SetIsEnabled(bHasRunnableCurrentCase);
}
if (RecordDnfButtonLabel != nullptr)
{
RecordDnfButtonLabel->SetText(FText::FromString(TEXT("Record DNF")));
}
if (RecordDnfButton != nullptr)
{
RecordDnfButton->SetIsEnabled(bHasRunnableCurrentCase);
}
if (UseTargetTimeButtonLabel != nullptr)
{
UseTargetTimeButtonLabel->SetText(FText::FromString(TEXT("Use Target")));
}
if (UseTargetTimeButton != nullptr)
{
UseTargetTimeButton->SetIsEnabled(bHasRunnableCurrentCase && CurrentTrainingCase.TimeTargetMs > 0);
}
if (UseAverageTimeButtonLabel != nullptr)
{
UseAverageTimeButtonLabel->SetText(FText::FromString(TEXT("Use Avg")));
}
if (UseAverageTimeButton != nullptr)
{
UseAverageTimeButton->SetIsEnabled(bHasActiveRun && CachedRunSummary.AverageTotalTimeMs > 0);
}
if (PreviousQueueButtonLabel != nullptr)
{
PreviousQueueButtonLabel->SetText(FText::FromString(TEXT("Prev Queue")));
@ -1144,6 +1378,92 @@ FString UHyperTwistCoachDashboardWidget::FindFirstDeferredQueueEntryId() const
return FString();
}
bool UHyperTwistCoachDashboardWidget::TryParseAttemptTimeOverride(int32& OutTimeMs) const
{
OutTimeMs = 0;
if (AttemptTimeTextBox == nullptr)
{
return false;
}
const FString RawText = AttemptTimeTextBox->GetText().ToString().TrimStartAndEnd();
if (RawText.IsEmpty())
{
return false;
}
int32 ParsedValue = 0;
if (!LexTryParseString(ParsedValue, *RawText))
{
return false;
}
OutTimeMs = FMath::Max(ParsedValue, 0);
return true;
}
int32 UHyperTwistCoachDashboardWidget::ResolveAttemptTimeMs(const EHyperTwistTrainingAttemptResult Result) const
{
int32 ParsedOverrideMs = 0;
if (TryParseAttemptTimeOverride(ParsedOverrideMs))
{
return ParsedOverrideMs;
}
const int32 TargetTimeMs = FMath::Max(CachedRunState.CurrentSelection.TrainingCase.TimeTargetMs, 0);
const int32 AverageTimeMs = FMath::Max(CachedRunSummary.AverageTotalTimeMs, 0);
switch (Result)
{
case EHyperTwistTrainingAttemptResult::Success:
if (TargetTimeMs > 0)
{
return TargetTimeMs;
}
if (AverageTimeMs > 0)
{
return AverageTimeMs;
}
return FMath::Max(DefaultSuccessAttemptTimeMs, 1);
case EHyperTwistTrainingAttemptResult::Failure:
if (AverageTimeMs > 0)
{
return FMath::Max(AverageTimeMs, DefaultFailureAttemptTimeMs);
}
if (TargetTimeMs > 0)
{
return FMath::Max(TargetTimeMs * 2, DefaultFailureAttemptTimeMs);
}
return FMath::Max(DefaultFailureAttemptTimeMs, 1);
case EHyperTwistTrainingAttemptResult::Timeout:
if (TargetTimeMs > 0)
{
return FMath::Max(TargetTimeMs * 2, DefaultTimeoutAttemptTimeMs);
}
return FMath::Max(DefaultTimeoutAttemptTimeMs, 1);
case EHyperTwistTrainingAttemptResult::DNF:
if (TargetTimeMs > 0)
{
return FMath::Max(TargetTimeMs * 2, DefaultDnfAttemptTimeMs);
}
return FMath::Max(DefaultDnfAttemptTimeMs, 1);
default:
return FMath::Max(DefaultFailureAttemptTimeMs, 1);
}
}
void UHyperTwistCoachDashboardWidget::SetAttemptTimeText(const int32 TimeMs)
{
if (AttemptTimeTextBox != nullptr)
{
AttemptTimeTextBox->SetText(FText::FromString(FString::FromInt(FMath::Max(TimeMs, 0))));
}
}
const FHyperTwistTrainingCoachSessionQueueStateEntry* UHyperTwistCoachDashboardWidget::FindSelectedQueueEntry() const
{
if (SelectedQueueEntryIndex < 0 || SelectedQueueEntryIndex >= CachedCoachSessionQueueState.Entries.Num())

View file

@ -5,6 +5,7 @@
#include "HyperTwistCoachDashboardWidget.generated.h"
class UButton;
class UEditableTextBox;
class UTextBlock;
class UVerticalBox;
@ -25,6 +26,12 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
int32 DefaultFailureAttemptTimeMs = 30000;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
int32 DefaultTimeoutAttemptTimeMs = 45000;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
int32 DefaultDnfAttemptTimeMs = 60000;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
bool bPreferActiveQueueSelectionOnRefresh = true;
@ -92,6 +99,15 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CurrentCaseNotationTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> AttemptControlHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> AttemptInputStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> AttemptTimeTextBox = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueStatusTextBlock = nullptr;
@ -176,6 +192,30 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> RecordFailureButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> RecordTimeoutButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> RecordTimeoutButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> RecordDnfButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> RecordDnfButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> UseTargetTimeButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> UseTargetTimeButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> UseAverageTimeButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> UseAverageTimeButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousQueueButton = nullptr;
@ -218,6 +258,18 @@ protected:
UFUNCTION()
void HandleRecordFailureClicked();
UFUNCTION()
void HandleRecordTimeoutClicked();
UFUNCTION()
void HandleRecordDnfClicked();
UFUNCTION()
void HandleUseTargetTimeClicked();
UFUNCTION()
void HandleUseAverageTimeClicked();
UFUNCTION()
void HandlePreviousQueueClicked();
@ -233,5 +285,8 @@ private:
void UpdateDashboardPresentation();
FString BuildDefaultDeferredUntilUtc() const;
FString FindFirstDeferredQueueEntryId() const;
bool TryParseAttemptTimeOverride(int32& OutTimeMs) const;
int32 ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult Result) const;
void SetAttemptTimeText(int32 TimeMs);
const FHyperTwistTrainingCoachSessionQueueStateEntry* FindSelectedQueueEntry() const;
};