Add queue browsing to coach dashboard

This commit is contained in:
axiomlogicnexus 2026-04-27 05:14:06 +02:00
parent ead48b43aa
commit 23fa842616
12 changed files with 693 additions and 6 deletions

View file

@ -11,6 +11,17 @@
namespace HyperTwistCoachDashboardWidgetInternal
{
template <typename TEnum>
FString EnumDisplayName(const TEnum Value)
{
if (const UEnum* Enum = StaticEnum<TEnum>())
{
return Enum->GetDisplayNameTextByValue(static_cast<int64>(Value)).ToString();
}
return TEXT("Unknown");
}
UTextBlock* AddTextRow(
UWidgetTree* WidgetTree,
UVerticalBox* Parent,
@ -75,6 +86,7 @@ void UHyperTwistCoachDashboardWidget::NativeConstruct()
void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView()
{
RefreshTrainingState();
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
@ -94,6 +106,7 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::ExecutePrimaryCoach
RunState = StartCoachRecommendedRun(DefaultCoachMaxCases);
}
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
return RunState;
}
@ -105,23 +118,90 @@ bool UHyperTwistCoachDashboardWidget::DeferDisplayedQueueEntryByDefaultWindow()
TEXT("dashboard-defer"),
true
);
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
return bSucceeded;
}
bool UHyperTwistCoachDashboardWidget::PromoteFirstDeferredQueueEntry()
{
const FString DeferredEntryId = FindFirstDeferredQueueEntryId();
FString DeferredEntryId;
if (const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry();
SelectedEntry != nullptr
&& SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred)
{
DeferredEntryId = SelectedEntry->EntryId;
}
else
{
DeferredEntryId = FindFirstDeferredQueueEntryId();
}
if (DeferredEntryId.IsEmpty())
{
return false;
}
const bool bSucceeded = PromoteCoachQueueEntry(DeferredEntryId);
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
return bSucceeded;
}
bool UHyperTwistCoachDashboardWidget::SelectPreviousQueueEntry()
{
if (CachedCoachSessionQueueState.Entries.Num() <= 0)
{
SelectedQueueEntryIndex = INDEX_NONE;
return false;
}
if (SelectedQueueEntryIndex == INDEX_NONE)
{
SyncSelectedQueueEntry();
return SelectedQueueEntryIndex != INDEX_NONE;
}
SelectedQueueEntryIndex =
(SelectedQueueEntryIndex - 1 + CachedCoachSessionQueueState.Entries.Num())
% CachedCoachSessionQueueState.Entries.Num();
UpdateDashboardPresentation();
return true;
}
bool UHyperTwistCoachDashboardWidget::SelectNextQueueEntry()
{
if (CachedCoachSessionQueueState.Entries.Num() <= 0)
{
SelectedQueueEntryIndex = INDEX_NONE;
return false;
}
if (SelectedQueueEntryIndex == INDEX_NONE)
{
SyncSelectedQueueEntry();
return SelectedQueueEntryIndex != INDEX_NONE;
}
SelectedQueueEntryIndex = (SelectedQueueEntryIndex + 1) % CachedCoachSessionQueueState.Entries.Num();
UpdateDashboardPresentation();
return true;
}
FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartSelectedQueueEntry()
{
if (const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry();
SelectedEntry != nullptr)
{
FHyperTwistTrainingRunState RunState =
StartCoachQueueEntry(SelectedEntry->EntryId, DefaultCoachMaxCases);
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
return RunState;
}
return FHyperTwistTrainingRunState();
}
void UHyperTwistCoachDashboardWidget::HandlePrimaryActionClicked()
{
ExecutePrimaryCoachAction();
@ -135,7 +215,21 @@ void UHyperTwistCoachDashboardWidget::HandleVerifyClicked()
void UHyperTwistCoachDashboardWidget::HandleDeferClicked()
{
DeferDisplayedQueueEntryByDefaultWindow();
if (const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry())
{
DeferCoachQueueEntry(
SelectedEntry->EntryId,
BuildDefaultDeferredUntilUtc(),
TEXT("dashboard-defer"),
true
);
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
else
{
DeferDisplayedQueueEntryByDefaultWindow();
}
}
void UHyperTwistCoachDashboardWidget::HandlePromoteClicked()
@ -143,6 +237,21 @@ void UHyperTwistCoachDashboardWidget::HandlePromoteClicked()
PromoteFirstDeferredQueueEntry();
}
void UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked()
{
SelectPreviousQueueEntry();
}
void UHyperTwistCoachDashboardWidget::HandleStartSelectedClicked()
{
StartSelectedQueueEntry();
}
void UHyperTwistCoachDashboardWidget::HandleNextQueueClicked()
{
SelectNextQueueEntry();
}
void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
{
if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
@ -176,18 +285,42 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
TEXT("CoachFocus"),
8
);
FollowUpTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachFollowUp"),
4
);
QueueStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachQueueStatus"),
2
);
QueueSelectionTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachQueueSelection"),
2
);
QueueEntryDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachQueueEntryDetail"),
2
);
QueueCountsTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachQueueCounts"),
2
);
QueueExecutionTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachQueueExecution"),
2
);
VerificationStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
@ -251,6 +384,51 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
PromoteButtonLabel = PromoteLabelRaw;
}
UHorizontalBox* QueueNavRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("CoachDashboardQueueNavButtons")
);
if (QueueNavRow != nullptr)
{
if (UVerticalBoxSlot* QueueNavRowSlot = RootLayout->AddChildToVerticalBox(QueueNavRow))
{
QueueNavRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f));
}
UTextBlock* PreviousQueueLabelRaw = nullptr;
PreviousQueueButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
QueueNavRow,
TEXT("PreviousQueueButton"),
TEXT("PreviousQueueButtonLabel"),
TEXT("Prev Queue"),
PreviousQueueLabelRaw
);
PreviousQueueButtonLabel = PreviousQueueLabelRaw;
UTextBlock* StartSelectedLabelRaw = nullptr;
StartSelectedButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
QueueNavRow,
TEXT("StartSelectedQueueButton"),
TEXT("StartSelectedQueueButtonLabel"),
TEXT("Start Selected"),
StartSelectedLabelRaw
);
StartSelectedButtonLabel = StartSelectedLabelRaw;
UTextBlock* NextQueueLabelRaw = nullptr;
NextQueueButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
QueueNavRow,
TEXT("NextQueueButton"),
TEXT("NextQueueButtonLabel"),
TEXT("Next Queue"),
NextQueueLabelRaw
);
NextQueueButtonLabel = NextQueueLabelRaw;
}
if (PrimaryActionButton != nullptr)
{
PrimaryActionButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePrimaryActionClicked);
@ -267,6 +445,72 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
{
PromoteButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePromoteClicked);
}
if (PreviousQueueButton != nullptr)
{
PreviousQueueButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked);
}
if (StartSelectedButton != nullptr)
{
StartSelectedButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleStartSelectedClicked);
}
if (NextQueueButton != nullptr)
{
NextQueueButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleNextQueueClicked);
}
}
void UHyperTwistCoachDashboardWidget::SyncSelectedQueueEntry()
{
if (CachedCoachSessionQueueState.Entries.Num() <= 0)
{
SelectedQueueEntryIndex = INDEX_NONE;
return;
}
if (SelectedQueueEntryIndex >= 0
&& SelectedQueueEntryIndex < CachedCoachSessionQueueState.Entries.Num())
{
return;
}
SelectedQueueEntryIndex = INDEX_NONE;
if (bPreferActiveQueueSelectionOnRefresh && !CachedCoachPanelState.ActiveQueueEntryId.IsEmpty())
{
SelectedQueueEntryIndex = CachedCoachSessionQueueState.Entries.IndexOfByPredicate(
[this](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate)
{
return Candidate.EntryId == CachedCoachPanelState.ActiveQueueEntryId;
}
);
}
if (SelectedQueueEntryIndex == INDEX_NONE)
{
SelectedQueueEntryIndex = CachedCoachSessionQueueState.Entries.IndexOfByPredicate(
[](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate)
{
return Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready
|| (Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending
&& Candidate.bCanStartImmediately);
}
);
}
if (SelectedQueueEntryIndex == INDEX_NONE)
{
SelectedQueueEntryIndex = CachedCoachSessionQueueState.Entries.IndexOfByPredicate(
[](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate)
{
return Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred;
}
);
}
if (SelectedQueueEntryIndex == INDEX_NONE)
{
SelectedQueueEntryIndex = 0;
}
}
void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
@ -288,17 +532,96 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
CachedCoachPanelState.FocusCaseCount
)));
}
if (FollowUpTextBlock != nullptr)
{
const FString FollowUpLine =
(!CachedCoachFollowUpBrief.UserId.IsEmpty() && !CachedCoachFollowUpBrief.Headline.IsEmpty())
? FString::Printf(
TEXT("Follow-up: %s | Action: %s | Cases: %d"),
*CachedCoachFollowUpBrief.Headline,
CachedCoachFollowUpBrief.PrimaryActionLabel.IsEmpty()
? TEXT("n/a")
: *CachedCoachFollowUpBrief.PrimaryActionLabel,
CachedCoachFollowUpBrief.FocusCaseIds.Num()
)
: TEXT("Follow-up: none queued beyond the current coach chain");
FollowUpTextBlock->SetText(FText::FromString(FollowUpLine));
}
if (QueueStatusTextBlock != nullptr)
{
QueueStatusTextBlock->SetText(FText::FromString(CachedCoachPanelState.QueueStatusLine));
}
if (QueueSelectionTextBlock != nullptr)
{
if (const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry())
{
QueueSelectionTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Selected queue item %d/%d | Source: %s | State: %s | Priority: %s (%.1f)"),
SelectedQueueEntryIndex + 1,
CachedCoachSessionQueueState.Entries.Num(),
SelectedEntry->SourceLabel.IsEmpty() ? TEXT("n/a") : *SelectedEntry->SourceLabel,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(SelectedEntry->EntryState),
SelectedEntry->PriorityLabel.IsEmpty() ? TEXT("n/a") : *SelectedEntry->PriorityLabel,
SelectedEntry->PriorityScore
)));
}
else
{
QueueSelectionTextBlock->SetText(FText::FromString(TEXT("Selected queue item: none")));
}
}
if (QueueEntryDetailTextBlock != nullptr)
{
if (const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry())
{
const FString DeferredLabel = SelectedEntry->DeferredUntilUtc.IsEmpty()
? TEXT("n/a")
: SelectedEntry->DeferredUntilUtc;
const FString DeferralReason = SelectedEntry->DeferralReasonLabel.IsEmpty()
? TEXT("n/a")
: SelectedEntry->DeferralReasonLabel;
QueueEntryDetailTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Headline: %s | Deck: %s | Method: %s | Cases: %d | Mode: %s | Policy: %s | Deferred until: %s | Reason: %s | Follow-up: %s | Archive tune: %s | Pinned: %s"),
*SelectedEntry->Headline,
SelectedEntry->FocusDeckId.IsEmpty() ? TEXT("n/a") : *SelectedEntry->FocusDeckId,
SelectedEntry->FocusMethodSegmentId.IsEmpty() ? TEXT("n/a") : *SelectedEntry->FocusMethodSegmentId,
SelectedEntry->FocusCaseIds.Num(),
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(SelectedEntry->SuggestedMode),
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(SelectedEntry->SuggestedSelectionPolicy),
*DeferredLabel,
*DeferralReason,
SelectedEntry->bRequiresFollowUp ? TEXT("yes") : TEXT("no"),
SelectedEntry->bRequiresArchiveRetentionTuning ? TEXT("yes") : TEXT("no"),
SelectedEntry->bPinned ? TEXT("yes") : TEXT("no")
)));
}
else
{
QueueEntryDetailTextBlock->SetText(FText::FromString(TEXT("Queue entry detail: no queue entry selected")));
}
}
if (QueueCountsTextBlock != nullptr)
{
QueueCountsTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Signals: %d | Follow-up queue entries: %d | Runnable: %s"),
TEXT("Signals: %d | Ready: %d | Deferred: %d | In-progress: %d | Completed: %d"),
CachedCoachPanelState.SignalCount,
CachedCoachPanelState.FollowUpQueueEntryCount,
CachedCoachPanelState.bHasRunnableQueueEntry ? TEXT("yes") : TEXT("no")
CachedCoachSessionQueueState.ReadyEntryCount,
CachedCoachSessionQueueState.DeferredEntryCount,
CachedCoachSessionQueueState.InProgressEntryCount,
CachedCoachSessionQueueState.CompletedEntryCount
)));
}
if (QueueExecutionTextBlock != nullptr)
{
QueueExecutionTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Execution: queued %d | started %d | completed %d | deferred %d | manual reschedules %d | outstanding deferred: %s | completion gap: %s"),
CachedCoachQueueExecutionSummary.QueuedEventCount,
CachedCoachQueueExecutionSummary.StartedEventCount,
CachedCoachQueueExecutionSummary.CompletedEventCount,
CachedCoachQueueExecutionSummary.DeferredEventCount,
CachedCoachQueueExecutionSummary.ManualRescheduleEventCount,
CachedCoachQueueExecutionSummary.bHasOutstandingDeferredWork ? TEXT("yes") : TEXT("no"),
CachedCoachQueueExecutionSummary.bHasCompletionGap ? TEXT("yes") : TEXT("no")
)));
}
if (VerificationStatusTextBlock != nullptr)
@ -334,7 +657,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
}
if (DeferButton != nullptr)
{
DeferButton->SetIsEnabled(CachedCoachPanelState.bHasRunnableQueueEntry);
DeferButton->SetIsEnabled(FindSelectedQueueEntry() != nullptr);
}
if (PromoteButtonLabel != nullptr)
{
@ -344,6 +667,35 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
PromoteButton->SetIsEnabled(!FindFirstDeferredQueueEntryId().IsEmpty());
}
if (PreviousQueueButtonLabel != nullptr)
{
PreviousQueueButtonLabel->SetText(FText::FromString(TEXT("Prev Queue")));
}
if (PreviousQueueButton != nullptr)
{
PreviousQueueButton->SetIsEnabled(CachedCoachSessionQueueState.Entries.Num() > 1);
}
if (StartSelectedButtonLabel != nullptr)
{
StartSelectedButtonLabel->SetText(FText::FromString(TEXT("Start Selected")));
}
if (StartSelectedButton != nullptr)
{
const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry();
const bool bSelectedRunnable = SelectedEntry != nullptr
&& (SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready
|| (SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending
&& SelectedEntry->bCanStartImmediately));
StartSelectedButton->SetIsEnabled(bSelectedRunnable);
}
if (NextQueueButtonLabel != nullptr)
{
NextQueueButtonLabel->SetText(FText::FromString(TEXT("Next Queue")));
}
if (NextQueueButton != nullptr)
{
NextQueueButton->SetIsEnabled(CachedCoachSessionQueueState.Entries.Num() > 1);
}
}
FString UHyperTwistCoachDashboardWidget::BuildDefaultDeferredUntilUtc() const
@ -365,3 +717,13 @@ FString UHyperTwistCoachDashboardWidget::FindFirstDeferredQueueEntryId() const
return FString();
}
const FHyperTwistTrainingCoachSessionQueueStateEntry* UHyperTwistCoachDashboardWidget::FindSelectedQueueEntry() const
{
if (SelectedQueueEntryIndex < 0 || SelectedQueueEntryIndex >= CachedCoachSessionQueueState.Entries.Num())
{
return nullptr;
}
return &CachedCoachSessionQueueState.Entries[SelectedQueueEntryIndex];
}

View file

@ -149,6 +149,21 @@ FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartNextQueuedCoach
return CachedRunState;
}
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachQueueEntry(
const FString& EntryId,
const int32 MaxCases
)
{
CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartCoachQueueEntry(
this,
EntryId,
DefaultSessionId + TEXT("_queue_") + EntryId,
MaxCases > 0 ? MaxCases : DefaultCoachMaxCases
);
RefreshTrainingState();
return CachedRunState;
}
bool UHyperTwistTrainingPanelWidget::DeferCoachQueueEntry(
const FString& EntryId,
const FString& DeferredUntilUtc,

View file

@ -6236,6 +6236,60 @@ FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState);
}
FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryById(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& EntryId,
const FString& TrainingSessionId,
const FString& StartedAtUtc
)
{
FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState =
HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState);
if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty())
{
return UpdatedQueueState;
}
const FString ResolvedStartedAtUtc =
HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(StartedAtUtc);
const int32 ActivatedIndex = UpdatedQueueState.Entries.IndexOfByPredicate(
[&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate)
{
return Candidate.EntryId == EntryId
&& Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Deferred
&& !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState);
}
);
if (ActivatedIndex == INDEX_NONE)
{
return UpdatedQueueState;
}
for (FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : UpdatedQueueState.Entries)
{
if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress)
{
Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending;
Entry.SourceTrainingSessionId.Reset();
Entry.StartedAtUtc.Reset();
Entry.bCanStartImmediately = false;
}
}
FHyperTwistTrainingCoachSessionQueueStateEntry& ActiveEntry =
UpdatedQueueState.Entries[ActivatedIndex];
ActiveEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::InProgress;
ActiveEntry.SourceTrainingSessionId = TrainingSessionId;
ActiveEntry.ScheduledAtUtc = !ActiveEntry.ScheduledAtUtc.IsEmpty()
? ActiveEntry.ScheduledAtUtc
: ResolvedStartedAtUtc;
ActiveEntry.DeferredUntilUtc.Reset();
ActiveEntry.StartedAtUtc = ResolvedStartedAtUtc;
ActiveEntry.bCanStartImmediately = false;
UpdatedQueueState.LastUpdatedAtUtc = ResolvedStartedAtUtc;
return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState);
}
FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::FinalizeCoachSessionQueueEntry(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& TrainingSessionId,

View file

@ -1043,6 +1043,21 @@ FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartNextQueuedCo
return FHyperTwistTrainingRunState();
}
FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartCoachQueueEntry(
UObject* WorldContextObject,
const FString& EntryId,
const FString& SessionId,
int32 MaxCases
)
{
if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject))
{
return TrainingSubsystem->StartCoachQueueEntry(EntryId, SessionId, MaxCases);
}
return FHyperTwistTrainingRunState();
}
FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartSampleClassicTrainingRun(
UObject* WorldContextObject,
const FString& UserId,

View file

@ -161,6 +161,21 @@ FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartNextQueuedCoac
return CachedRunState;
}
FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartCoachQueueEntryTrainingRun(
const FString& EntryId,
const int32 MaxCases
)
{
CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartCoachQueueEntry(
this,
EntryId,
AutoStartSessionId + TEXT("_queue_") + EntryId,
MaxCases > 0 ? MaxCases : DefaultCoachMaxCases
);
RefreshCachedTrainingState();
return CachedRunState;
}
bool AHyperTwistTrainingSessionActor::DeferCoachQueueEntry(
const FString& EntryId,
const FString& DeferredUntilUtc,

View file

@ -75,6 +75,24 @@ namespace HyperTwistTrainingSubsystemInternal
return BestEntry;
}
const FHyperTwistTrainingCoachSessionQueueStateEntry* FindCoachQueueEntryById(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& EntryId
)
{
if (!QueueState.IsStructurallyValid() || EntryId.IsEmpty())
{
return nullptr;
}
return QueueState.Entries.FindByPredicate(
[&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate)
{
return Candidate.EntryId == EntryId;
}
);
}
FHyperTwistTrainingDeck BuildCoachQueueDeckFromEntry(
const FHyperTwistTrainingCoachSessionQueueStateEntry& QueueEntry,
const FHyperTwistTrainingRunState& ActiveRunState,
@ -1469,6 +1487,123 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartNextQueuedCoachRu
return RunState;
}
FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachQueueEntry(
const FString& EntryId,
const FString& SessionId,
int32 MaxCases
)
{
const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry =
HyperTwistTrainingSubsystemInternal::FindCoachQueueEntryById(ActiveCoachSessionQueueState, EntryId);
if (SelectedEntry == nullptr)
{
return FHyperTwistTrainingRunState();
}
const bool bIsRunnable = SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready
|| (SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending
&& SelectedEntry->bCanStartImmediately);
if (!bIsRunnable)
{
return FHyperTwistTrainingRunState();
}
FHyperTwistTrainingDeck QueueDeck;
FHyperTwistCoachBrief QueueBrief;
const bool bHasPrimaryCoachBrief = !ActiveCoachBrief.UserId.IsEmpty() && !ActiveCoachBrief.Headline.IsEmpty();
const bool bHasFollowUpCoachBrief =
!ActiveCoachFollowUpBrief.UserId.IsEmpty() && !ActiveCoachFollowUpBrief.Headline.IsEmpty();
FString UserId = !ActiveCoachSessionQueueState.UserId.IsEmpty()
? ActiveCoachSessionQueueState.UserId
: (ActiveRunState.Session.UserId.IsEmpty() ? TEXT("local-user") : ActiveRunState.Session.UserId);
if (SelectedEntry->SourceLabel == TEXT("primary-brief") && bHasPrimaryCoachBrief)
{
QueueDeck = BuildActiveCoachRecommendedDeck(MaxCases);
QueueBrief = ActiveCoachBrief;
UserId = ActiveCoachBrief.UserId.IsEmpty() ? UserId : ActiveCoachBrief.UserId;
}
else if (SelectedEntry->SourceLabel == TEXT("follow-up-brief") && bHasFollowUpCoachBrief)
{
QueueDeck = BuildActiveCoachFollowUpDeck(MaxCases);
QueueBrief = ActiveCoachFollowUpBrief;
UserId = ActiveCoachFollowUpBrief.UserId.IsEmpty() ? UserId : ActiveCoachFollowUpBrief.UserId;
}
else
{
QueueDeck = HyperTwistTrainingSubsystemInternal::BuildCoachQueueDeckFromEntry(
*SelectedEntry,
ActiveRunState,
MaxCases
);
QueueBrief = HyperTwistTrainingSubsystemInternal::BuildCoachBriefFromQueueEntry(
ActiveCoachSessionQueueState,
*SelectedEntry,
ActiveCoachSessionQueueState.ReferenceUtc
);
}
if (!QueueDeck.IsStructurallyValid())
{
return FHyperTwistTrainingRunState();
}
const FHyperTwistTrainingCoachSessionQueueState QueueStateBeforeStart = ActiveCoachSessionQueueState;
const FHyperTwistTrainingCoachActionPlan DraftActionPlan =
(!QueueBrief.UserId.IsEmpty() && !QueueBrief.Headline.IsEmpty())
? UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan(
QueueBrief,
FString::Printf(TEXT("coach_queue_%s_%s"), *SelectedEntry->EntryId, *SessionId),
FString()
)
: FHyperTwistTrainingCoachActionPlan();
FHyperTwistTrainingRunState RunState = StartTrainingRunFromDeck(
QueueDeck,
UserId,
SessionId,
SelectedEntry->SuggestedMode
);
if (!RunState.IsStructurallyValid())
{
return RunState;
}
if (DraftActionPlan.IsStructurallyValid())
{
ActiveCoachActionPlan = UHyperTwistTrainingRepositoryLibrary::ActivateCoachActionPlan(
DraftActionPlan,
RunState.Session.TrainingSessionId,
RunState.Session.StartedAtUtc
);
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachActionPlan(
TrainingRepositoryState,
ActiveCoachActionPlan
);
}
if (QueueStateBeforeStart.IsStructurallyValid())
{
const FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState =
UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryById(
QueueStateBeforeStart,
SelectedEntry->EntryId,
RunState.Session.TrainingSessionId,
RunState.Session.StartedAtUtc
);
if (UpdatedQueueState.IsStructurallyValid())
{
TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState(
TrainingRepositoryState,
UpdatedQueueState
);
ActiveCoachSessionQueueState = UpdatedQueueState;
}
}
RefreshRepositoryViews();
return RunState;
}
FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartTrainingRunFromDeck(
const FHyperTwistTrainingDeck& Deck,
const FString& UserId,

View file

@ -19,6 +19,12 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
int32 DefaultDeferredHours = 24;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
bool bPreferActiveQueueSelectionOnRefresh = true;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
int32 SelectedQueueEntryIndex = INDEX_NONE;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
void RefreshCoachDashboardView();
@ -31,6 +37,15 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool PromoteFirstDeferredQueueEntry();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool SelectPreviousQueueEntry();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool SelectNextQueueEntry();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
FHyperTwistTrainingRunState StartSelectedQueueEntry();
protected:
UPROPERTY(Transient)
TObjectPtr<UVerticalBox> RootLayout = nullptr;
@ -53,6 +68,18 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueCountsTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> FollowUpTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueSelectionTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueEntryDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueExecutionTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PrimaryActionButton = nullptr;
@ -77,6 +104,24 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PromoteButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousQueueButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PreviousQueueButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> StartSelectedButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> StartSelectedButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> NextQueueButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> NextQueueButtonLabel = nullptr;
UFUNCTION()
void HandlePrimaryActionClicked();
@ -89,9 +134,20 @@ protected:
UFUNCTION()
void HandlePromoteClicked();
UFUNCTION()
void HandlePreviousQueueClicked();
UFUNCTION()
void HandleStartSelectedClicked();
UFUNCTION()
void HandleNextQueueClicked();
private:
void EnsureDefaultDashboardBuilt();
void SyncSelectedQueueEntry();
void UpdateDashboardPresentation();
FString BuildDefaultDeferredUntilUtc() const;
FString FindFirstDeferredQueueEntryId() const;
const FHyperTwistTrainingCoachSessionQueueStateEntry* FindSelectedQueueEntry() const;
};

View file

@ -118,6 +118,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartNextQueuedCoachRun(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartCoachQueueEntry(
const FString& EntryId,
int32 MaxCases
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool DeferCoachQueueEntry(
const FString& EntryId,

View file

@ -369,6 +369,14 @@ public:
const FString& StartedAtUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingCoachSessionQueueState StartCoachSessionQueueEntryById(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,
const FString& EntryId,
const FString& TrainingSessionId,
const FString& StartedAtUtc
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach")
static FHyperTwistTrainingCoachSessionQueueState FinalizeCoachSessionQueueEntry(
const FHyperTwistTrainingCoachSessionQueueState& QueueState,

View file

@ -424,6 +424,14 @@ public:
int32 MaxCases
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject"))
static FHyperTwistTrainingRunState StartCoachQueueEntry(
UObject* WorldContextObject,
const FString& EntryId,
const FString& SessionId,
int32 MaxCases
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject"))
static FHyperTwistTrainingRunState StartSampleClassicTrainingRun(
UObject* WorldContextObject,

View file

@ -120,6 +120,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartNextQueuedCoachTrainingRun(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartCoachQueueEntryTrainingRun(
const FString& EntryId,
int32 MaxCases
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool DeferCoachQueueEntry(
const FString& EntryId,

View file

@ -342,6 +342,13 @@ public:
int32 MaxCases
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartCoachQueueEntry(
const FString& EntryId,
const FString& SessionId,
int32 MaxCases
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training")
FHyperTwistTrainingRunState StartTrainingRunFromDeck(
const FHyperTwistTrainingDeck& Deck,