Add dashboard next-step transition flow

This commit is contained in:
axiomlogicnexus 2026-04-27 14:38:45 +02:00
parent 7e43c6eb66
commit 6a95e0a170
3 changed files with 163 additions and 1 deletions

View file

@ -294,6 +294,16 @@ FHyperTwistTrainingRunStepResult UHyperTwistCoachDashboardWidget::SubmitSyntheti
return SubmitTrainingAttempt(Attempt, TimeMs);
}
bool UHyperTwistCoachDashboardWidget::ContinueCurrentRunFlow()
{
const bool bSucceeded = StartLiveAttemptTimer();
if (!bSucceeded)
{
UpdateDashboardPresentation();
}
return bSucceeded;
}
bool UHyperTwistCoachDashboardWidget::StartLiveAttemptTimer()
{
if (!CachedRunState.Session.IsStructurallyValid() || !CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid())
@ -483,6 +493,11 @@ void UHyperTwistCoachDashboardWidget::HandleCompleteRunClicked()
UpdateDashboardPresentation();
}
void UHyperTwistCoachDashboardWidget::HandleContinueRunClicked()
{
ContinueCurrentRunFlow();
}
void UHyperTwistCoachDashboardWidget::HandleClearRunClicked()
{
ClearActiveTrainingRun();
@ -706,6 +721,18 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
TEXT("CoachCurrentCaseNotation"),
2
);
TransitionHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachTransitionHeader"),
2
);
TransitionStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachTransitionStatus"),
2
);
AttemptControlHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
@ -1005,6 +1032,17 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
);
CompleteRunButtonLabel = CompleteRunLabelRaw;
UTextBlock* ContinueRunLabelRaw = nullptr;
ContinueRunButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
RunControlRow,
TEXT("ContinueRunButton"),
TEXT("ContinueRunButtonLabel"),
TEXT("Continue Run"),
ContinueRunLabelRaw
);
ContinueRunButtonLabel = ContinueRunLabelRaw;
UTextBlock* ClearRunLabelRaw = nullptr;
ClearRunButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
@ -1126,6 +1164,10 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
{
CompleteRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleCompleteRunClicked);
}
if (ContinueRunButton != nullptr)
{
ContinueRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleContinueRunClicked);
}
if (ClearRunButton != nullptr)
{
ClearRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleClearRunClicked);
@ -1328,6 +1370,83 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
}
LastAttemptPhaseSummary = FString::Join(PhaseParts, TEXT(" | "));
}
const bool bHasSessionScopedLastStep = !LastStepResult.SessionSummary.TrainingSessionId.IsEmpty()
&& LastStepResult.SessionSummary.TrainingSessionId == CachedRunState.Session.TrainingSessionId;
const bool bLastStepCompletedRun = bHasSessionScopedLastStep
&& (LastStepResult.AttemptResolution.UpdatedSession.SessionState
== EHyperTwistTrainingSessionState::Completed
|| LastStepResult.AttemptResolution.UpdatedSession.SessionState
== EHyperTwistTrainingSessionState::Aborted);
const bool bHasNextCaseFromLastStep = bHasSessionScopedLastStep
&& LastStepResult.AttemptResolution.NextCaseSelection.IsStructurallyValid();
const bool bCanContinueCurrentRun = !bHasLiveAttemptTimer
&& bHasRunnableCurrentCase
&& !bLastStepCompletedRun;
FString TransitionStatusLine;
if (bLiveInspectionPhase)
{
TransitionStatusLine = FString::Printf(
TEXT("Transition: inspection is live on case %s. Click Start Solve when ready, or cancel the timer."),
*ActiveCaseId
);
}
else if (bLiveSolvePhase)
{
TransitionStatusLine = FString::Printf(
TEXT("Transition: solve is live on case %s. Capture splits if needed, then stop with the final result."),
*ActiveCaseId
);
}
else if (bLastStepCompletedRun)
{
TransitionStatusLine = FString::Printf(
TEXT("Transition: run completed after case %s. Clear the finished run or launch the next queued coaching item."),
LastStepResult.SessionSummary.LastCaseId.IsEmpty() ? TEXT("n/a") : *LastStepResult.SessionSummary.LastCaseId
);
}
else if (bCanContinueCurrentRun)
{
const FHyperTwistTrainingCase& TransitionCase = CachedRunState.CurrentSelection.TrainingCase;
const FString TransitionPrompt = TransitionCase.PromptLabel.IsEmpty()
? ActiveCaseId
: TransitionCase.PromptLabel;
if (bHasNextCaseFromLastStep)
{
TransitionStatusLine = FString::Printf(
TEXT("Transition: next case loaded after the last attempt. Continue the run to begin %s."),
*TransitionPrompt
);
}
else if (CachedRunSummary.AttemptCount > 0)
{
TransitionStatusLine = FString::Printf(
TEXT("Transition: the current run is ready for another attempt on %s."),
*TransitionPrompt
);
}
else
{
TransitionStatusLine = FString::Printf(
TEXT("Transition: the run is staged on %s. Start the first timed attempt when ready."),
*TransitionPrompt
);
}
}
else if (bHasActiveRun)
{
TransitionStatusLine =
TEXT("Transition: this run has no runnable current case. Complete, clear, or relaunch from coach guidance.");
}
else
{
TransitionStatusLine =
TEXT("Transition: no run is active. Start recommended, follow-up, or queued coaching work from this screen.");
}
const FString ContinueRunLabel = bHasLiveAttemptTimer
? (bLiveInspectionPhase ? TEXT("Start Solve") : TEXT("Attempt Active"))
: (bHasNextCaseFromLastStep
? TEXT("Continue Run")
: (CachedRunSummary.AttemptCount > 0 ? TEXT("Start Next Attempt") : TEXT("Start First Attempt")));
if (HeadlineTextBlock != nullptr)
{
@ -1382,7 +1501,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
}
if (LastAttemptTextBlock != nullptr)
{
if (!LastStepResult.SessionSummary.TrainingSessionId.IsEmpty())
if (bHasSessionScopedLastStep)
{
LastAttemptTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Last recorded attempt: case %s | completed at %s | session completed attempts %d"),
@ -1484,6 +1603,14 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
));
}
}
if (TransitionHeaderTextBlock != nullptr)
{
TransitionHeaderTextBlock->SetText(FText::FromString(TEXT("[Next Step]")));
}
if (TransitionStatusTextBlock != nullptr)
{
TransitionStatusTextBlock->SetText(FText::FromString(TransitionStatusLine));
}
if (AttemptControlHeaderTextBlock != nullptr)
{
AttemptControlHeaderTextBlock->SetText(FText::FromString(TEXT("[Attempt Controls]")));
@ -1812,6 +1939,17 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
CompleteRunButton->SetIsEnabled(bHasActiveRun && !bHasLiveAttemptTimer);
}
if (ContinueRunButtonLabel != nullptr)
{
ContinueRunButtonLabel->SetText(FText::FromString(ContinueRunLabel));
}
if (ContinueRunButton != nullptr)
{
ContinueRunButton->SetIsEnabled(
(!bHasLiveAttemptTimer && bCanContinueCurrentRun)
|| (bLiveInspectionPhase && bHasRunnableCurrentCase)
);
}
if (ClearRunButtonLabel != nullptr)
{
ClearRunButtonLabel->SetText(FText::FromString(TEXT("Clear Run")));

View file

@ -41,6 +41,7 @@ void UHyperTwistTrainingPanelWidget::RefreshTrainingState()
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartSampleClassicTrainingRun()
{
LastStepResult = FHyperTwistTrainingRunStepResult();
CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartSampleClassicTrainingRun(
this,
DefaultUserId,
@ -58,6 +59,7 @@ FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartTrainingRunFrom
EHyperTwistTrainingDeliveryMode Mode
)
{
LastStepResult = FHyperTwistTrainingRunStepResult();
CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartTrainingRunFromDeck(
this,
Deck,
@ -118,6 +120,7 @@ FHyperTwistTrainingDeck UHyperTwistTrainingPanelWidget::BuildActiveCoachFollowUp
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachRecommendedRun(const int32 MaxCases)
{
LastStepResult = FHyperTwistTrainingRunStepResult();
CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartCoachRecommendedRun(
this,
DefaultSessionId + TEXT("_coach"),
@ -129,6 +132,7 @@ FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachRecommende
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachFollowUpRun(const int32 MaxCases)
{
LastStepResult = FHyperTwistTrainingRunStepResult();
CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartCoachFollowUpRun(
this,
DefaultSessionId + TEXT("_follow_up"),
@ -140,6 +144,7 @@ FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachFollowUpRu
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartNextQueuedCoachRun(const int32 MaxCases)
{
LastStepResult = FHyperTwistTrainingRunStepResult();
CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartNextQueuedCoachRun(
this,
DefaultSessionId + TEXT("_queue"),
@ -154,6 +159,7 @@ FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachQueueEntry
const int32 MaxCases
)
{
LastStepResult = FHyperTwistTrainingRunStepResult();
CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartCoachQueueEntry(
this,
EntryId,

View file

@ -91,6 +91,9 @@ public:
int32 TimeMs
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool ContinueCurrentRunFlow();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool StartLiveAttemptTimer();
@ -151,6 +154,12 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CurrentCaseNotationTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TransitionHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TransitionStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> AttemptControlHeaderTextBlock = nullptr;
@ -238,6 +247,12 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CompleteRunButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> ContinueRunButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ContinueRunButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> ClearRunButton = nullptr;
@ -337,6 +352,9 @@ protected:
UFUNCTION()
void HandleCompleteRunClicked();
UFUNCTION()
void HandleContinueRunClicked();
UFUNCTION()
void HandleClearRunClicked();