diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 0040379..d5568fd 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -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"))); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp index 1c45936..fed0eff 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp @@ -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, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index af3986c..cfbe531 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -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 CurrentCaseNotationTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr TransitionHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr TransitionStatusTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr AttemptControlHeaderTextBlock = nullptr; @@ -238,6 +247,12 @@ protected: UPROPERTY(Transient) TObjectPtr CompleteRunButtonLabel = nullptr; + UPROPERTY(Transient) + TObjectPtr ContinueRunButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr ContinueRunButtonLabel = nullptr; + UPROPERTY(Transient) TObjectPtr ClearRunButton = nullptr; @@ -337,6 +352,9 @@ protected: UFUNCTION() void HandleCompleteRunClicked(); + UFUNCTION() + void HandleContinueRunClicked(); + UFUNCTION() void HandleClearRunClicked();