diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp index 02af8bf..a8bc345 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp @@ -29,6 +29,7 @@ void UHyperTwistTrainingPanelWidget::RefreshTrainingState() UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSchedulePolicySummary(this); CachedCoachQueueExecutionSummary = UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSessionQueueExecutionSummary(this); + CachedCoachPanelState = UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachPanelState(this); CachedRepositoryRoundTripVerification = UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingRepositoryRoundTripVerification(this); } @@ -132,6 +133,17 @@ FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachFollowUpRu return CachedRunState; } +FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartNextQueuedCoachRun(const int32 MaxCases) +{ + CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartNextQueuedCoachRun( + this, + DefaultSessionId + TEXT("_queue"), + MaxCases > 0 ? MaxCases : DefaultCoachMaxCases + ); + RefreshTrainingState(); + return CachedRunState; +} + FHyperTwistTrainingRepositoryRoundTripVerification UHyperTwistTrainingPanelWidget::VerifyTrainingRepositoryRoundTrip() { diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp index b173732..928a4b9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -127,6 +127,18 @@ UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingRepositoryRoundTripVerificat return FHyperTwistTrainingRepositoryRoundTripVerification(); } +FHyperTwistTrainingCoachPanelState UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachPanelState( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveCoachPanelState(); + } + + return FHyperTwistTrainingCoachPanelState(); +} + FHyperTwistTrainingDeckHistorySummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingDeckHistorySummary(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) @@ -1017,6 +1029,20 @@ FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartCoachFollowU return FHyperTwistTrainingRunState(); } +FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartNextQueuedCoachRun( + UObject* WorldContextObject, + const FString& SessionId, + int32 MaxCases +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->StartNextQueuedCoachRun(SessionId, MaxCases); + } + + return FHyperTwistTrainingRunState(); +} + FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartSampleClassicTrainingRun( UObject* WorldContextObject, const FString& UserId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp index 6b03967..c282e81 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp @@ -92,6 +92,7 @@ void AHyperTwistTrainingSessionActor::RefreshCachedTrainingState() UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSchedulePolicySummary(this); CachedCoachQueueExecutionSummary = UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSessionQueueExecutionSummary(this); + CachedCoachPanelState = UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachPanelState(this); CachedRepositoryRoundTripVerification = UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingRepositoryRoundTripVerification(this); } @@ -142,6 +143,19 @@ FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartCoachFollowUpT return CachedRunState; } +FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartNextQueuedCoachTrainingRun( + const int32 MaxCases +) +{ + CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartNextQueuedCoachRun( + this, + AutoStartSessionId + TEXT("_queue"), + MaxCases > 0 ? MaxCases : DefaultCoachMaxCases + ); + RefreshCachedTrainingState(); + return CachedRunState; +} + FHyperTwistTrainingRepositoryRoundTripVerification AHyperTwistTrainingSessionActor::VerifyRepositoryRoundTrip() { diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 19770a9..aded721 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -11,6 +11,192 @@ namespace HyperTwistTrainingSubsystemInternal { const TCHAR* ActiveCoachSessionQueueId = TEXT("coach_queue_active"); + + EHyperTwistCoachPriority CoachPriorityFromScore(const float Score) + { + if (Score >= 85.0f) + { + return EHyperTwistCoachPriority::Critical; + } + if (Score >= 65.0f) + { + return EHyperTwistCoachPriority::Elevated; + } + if (Score >= 40.0f) + { + return EHyperTwistCoachPriority::Advisory; + } + return EHyperTwistCoachPriority::Informational; + } + + FString ResolveQueueActionLabel(const FString& SourceLabel) + { + if (SourceLabel == TEXT("follow-up-brief")) + { + return TEXT("Start coach follow-up run"); + } + if (SourceLabel == TEXT("archive-policy")) + { + return TEXT("Start archive retention run"); + } + if (SourceLabel == TEXT("primary-brief")) + { + return TEXT("Start coach recommended run"); + } + return TEXT("Start queued coach run"); + } + + const FHyperTwistTrainingCoachSessionQueueStateEntry* FindNextRunnableCoachQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState + ) + { + if (!QueueState.IsStructurallyValid()) + { + return nullptr; + } + + const FHyperTwistTrainingCoachSessionQueueStateEntry* BestEntry = nullptr; + for (const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate : QueueState.Entries) + { + const bool bIsRunnable = Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready + || (Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending + && Candidate.bCanStartImmediately); + if (!bIsRunnable) + { + continue; + } + + if (BestEntry == nullptr || Candidate.SlotIndex < BestEntry->SlotIndex) + { + BestEntry = &Candidate; + } + } + + return BestEntry; + } + + FHyperTwistTrainingDeck BuildCoachQueueDeckFromEntry( + const FHyperTwistTrainingCoachSessionQueueStateEntry& QueueEntry, + const FHyperTwistTrainingRunState& ActiveRunState, + int32 MaxCases + ) + { + const int32 DesiredCaseCount = MaxCases > 0 ? MaxCases : 3; + FHyperTwistTrainingDeck SourceDeck; + if (!QueueEntry.FocusDeckId.IsEmpty()) + { + UHyperTwistTrainingCatalogLibrary::TryFindDeckInCatalog( + UHyperTwistTrainingCatalogLibrary::MakePhase3TrainingCatalog(), + QueueEntry.FocusDeckId, + SourceDeck + ); + } + + if (!SourceDeck.IsStructurallyValid() + && ActiveRunState.ActiveDeck.IsStructurallyValid() + && (QueueEntry.FocusDeckId.IsEmpty() + || QueueEntry.FocusDeckId == ActiveRunState.ActiveDeck.DeckId)) + { + SourceDeck = ActiveRunState.ActiveDeck; + } + + if (!SourceDeck.IsStructurallyValid()) + { + return FHyperTwistTrainingDeck(); + } + + TArray SelectedCases; + if (QueueEntry.FocusCaseIds.Num() > 0) + { + for (const FString& FocusCaseId : QueueEntry.FocusCaseIds) + { + const FHyperTwistTrainingCase* MatchingCase = SourceDeck.Cases.FindByPredicate( + [&FocusCaseId](const FHyperTwistTrainingCase& Candidate) + { + return Candidate.CaseId == FocusCaseId; + } + ); + if (MatchingCase != nullptr) + { + SelectedCases.Add(*MatchingCase); + if (SelectedCases.Num() >= DesiredCaseCount) + { + break; + } + } + } + } + + if (SelectedCases.Num() == 0) + { + for (const FHyperTwistTrainingCase& Candidate : SourceDeck.Cases) + { + SelectedCases.Add(Candidate); + if (SelectedCases.Num() >= DesiredCaseCount) + { + break; + } + } + } + + if (SelectedCases.Num() == 0) + { + return FHyperTwistTrainingDeck(); + } + + FHyperTwistTrainingDeck FocusDeck = SourceDeck; + FocusDeck.Cases = MoveTemp(SelectedCases); + FocusDeck.SelectionPolicy = QueueEntry.SuggestedSelectionPolicy; + if (QueueEntry.SuggestedMode != EHyperTwistTrainingDeliveryMode::Timer + && !FocusDeck.DeliveryModes.Contains(QueueEntry.SuggestedMode)) + { + FocusDeck.DeliveryModes.Insert(QueueEntry.SuggestedMode, 0); + } + if (!QueueEntry.Headline.IsEmpty()) + { + FocusDeck.Title = FString::Printf(TEXT("%s - %s"), *SourceDeck.Title, *QueueEntry.Headline); + } + if (!FocusDeck.Tags.Contains(TEXT("coach-queue"))) + { + FocusDeck.Tags.Add(TEXT("coach-queue")); + } + if (!QueueEntry.SourceLabel.IsEmpty()) + { + FocusDeck.Tags.AddUnique(FString::Printf(TEXT("coach-queue:%s"), *QueueEntry.SourceLabel.ToLower())); + } + if (!QueueEntry.FocusMethodSegmentId.IsEmpty()) + { + FocusDeck.SubsetId = QueueEntry.FocusMethodSegmentId; + } + return FocusDeck; + } + + FHyperTwistCoachBrief BuildCoachBriefFromQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FHyperTwistTrainingCoachSessionQueueStateEntry& QueueEntry, + const FString& ReferenceUtc + ) + { + FHyperTwistCoachBrief Brief; + Brief.UserId = QueueState.UserId; + Brief.ReferenceUtc = ReferenceUtc; + Brief.PriorityScore = QueueEntry.PriorityScore; + Brief.Priority = CoachPriorityFromScore(QueueEntry.PriorityScore); + Brief.FocusLane = QueueEntry.SourceLabel == TEXT("archive-policy") + ? EHyperTwistCoachFocusLane::Stability + : EHyperTwistCoachFocusLane::ReviewBacklog; + Brief.Headline = QueueEntry.Headline; + Brief.PrimaryActionLabel = ResolveQueueActionLabel(QueueEntry.SourceLabel); + Brief.SuggestedMode = QueueEntry.SuggestedMode; + Brief.SuggestedSelectionPolicy = QueueEntry.SuggestedSelectionPolicy; + Brief.FocusDeckId = QueueEntry.FocusDeckId; + Brief.FocusMethodSegmentId = QueueEntry.FocusMethodSegmentId; + Brief.FocusCaseIds = QueueEntry.FocusCaseIds; + Brief.bNeedsCoachReview = QueueEntry.SuggestedMode == EHyperTwistTrainingDeliveryMode::CoachReviewed; + Brief.bPreferRecognitionReplayReview = + QueueEntry.SuggestedMode == EHyperTwistTrainingDeliveryMode::RecognitionAssisted; + return Brief; + } } bool UHyperTwistTrainingSubsystem::HasActiveRun() const @@ -59,6 +245,131 @@ UHyperTwistTrainingSubsystem::GetActiveRepositoryRoundTripVerification() const return ActiveRepositoryRoundTripVerification; } +FHyperTwistTrainingCoachPanelState UHyperTwistTrainingSubsystem::GetActiveCoachPanelState() const +{ + FHyperTwistTrainingCoachPanelState PanelState; + PanelState.UserId = !ActiveCoachBrief.UserId.IsEmpty() + ? ActiveCoachBrief.UserId + : (!ActiveCoachSessionQueueSummary.UserId.IsEmpty() + ? ActiveCoachSessionQueueSummary.UserId + : ActiveRunState.Session.UserId); + PanelState.bHasActiveRun = HasActiveRun(); + PanelState.bHasCoachGuidance = !ActiveCoachBrief.UserId.IsEmpty() && !ActiveCoachBrief.Headline.IsEmpty(); + PanelState.bHasFollowUpGuidance = + !ActiveCoachFollowUpBrief.UserId.IsEmpty() && !ActiveCoachFollowUpBrief.Headline.IsEmpty(); + PanelState.SignalCount = ActiveCoachSignals.Num(); + PanelState.bRepositoryVerificationPassed = ActiveRepositoryRoundTripVerification.bDidRoundTripPass; + PanelState.bRepositoryVerificationNeedsAttention = ActiveRepositoryRoundTripVerification.ResolvedPath.IsEmpty() + || !ActiveRepositoryRoundTripVerification.bDidRoundTripPass; + + const FHyperTwistTrainingCoachSessionQueueStateEntry* NextRunnableEntry = + HyperTwistTrainingSubsystemInternal::FindNextRunnableCoachQueueEntry(ActiveCoachSessionQueueState); + PanelState.bHasRunnableQueueEntry = NextRunnableEntry != nullptr; + PanelState.bHasQueueWork = ActiveCoachSessionQueueState.IsStructurallyValid() + ? ActiveCoachSessionQueueState.TotalEntryCount > 0 + : ActiveCoachSessionQueueSummary.TotalEntryCount > 0; + PanelState.bHasDeferredQueueWork = ActiveCoachSessionQueueState.IsStructurallyValid() + ? ActiveCoachSessionQueueState.DeferredEntryCount > 0 + : false; + PanelState.TotalQueueEntryCount = ActiveCoachSessionQueueState.IsStructurallyValid() + ? ActiveCoachSessionQueueState.TotalEntryCount + : ActiveCoachSessionQueueSummary.TotalEntryCount; + PanelState.ReadyQueueEntryCount = ActiveCoachSessionQueueState.IsStructurallyValid() + ? ActiveCoachSessionQueueState.ReadyEntryCount + : (ActiveCoachSessionQueueSummary.bHasImmediateQueue ? 1 : 0); + PanelState.DeferredQueueEntryCount = ActiveCoachSessionQueueState.IsStructurallyValid() + ? ActiveCoachSessionQueueState.DeferredEntryCount + : 0; + PanelState.FollowUpQueueEntryCount = ActiveCoachSessionQueueState.IsStructurallyValid() + ? ActiveCoachSessionQueueState.Entries.FilterByPredicate( + [](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.bRequiresFollowUp; + } + ).Num() + : ActiveCoachSessionQueueSummary.FollowUpEntryCount; + + const FHyperTwistTrainingDeck RecommendedDeck = BuildActiveCoachRecommendedDeck(3); + const FHyperTwistTrainingDeck FollowUpDeck = BuildActiveCoachFollowUpDeck(3); + PanelState.bCanStartCoachRecommendedRun = RecommendedDeck.IsStructurallyValid(); + PanelState.bCanStartCoachFollowUpRun = FollowUpDeck.IsStructurallyValid(); + PanelState.bCanStartQueuedRun = NextRunnableEntry != nullptr + && ((NextRunnableEntry->SourceLabel == TEXT("primary-brief") && PanelState.bCanStartCoachRecommendedRun) + || (NextRunnableEntry->SourceLabel == TEXT("follow-up-brief") && PanelState.bCanStartCoachFollowUpRun) + || HyperTwistTrainingSubsystemInternal::BuildCoachQueueDeckFromEntry( + *NextRunnableEntry, + ActiveRunState, + 3 + ).IsStructurallyValid()); + + if (NextRunnableEntry != nullptr) + { + PanelState.Headline = NextRunnableEntry->Headline; + PanelState.SecondaryLine = HyperTwistTrainingSubsystemInternal::ResolveQueueActionLabel( + NextRunnableEntry->SourceLabel + ); + PanelState.PrimaryActionLabel = PanelState.SecondaryLine; + PanelState.NextQueueSourceLabel = NextRunnableEntry->SourceLabel; + PanelState.ActiveQueueEntryId = NextRunnableEntry->EntryId; + PanelState.FocusDeckId = NextRunnableEntry->FocusDeckId; + PanelState.FocusMethodSegmentId = NextRunnableEntry->FocusMethodSegmentId; + PanelState.FocusCaseCount = NextRunnableEntry->FocusCaseIds.Num(); + PanelState.SuggestedMode = NextRunnableEntry->SuggestedMode; + PanelState.SuggestedSelectionPolicy = NextRunnableEntry->SuggestedSelectionPolicy; + } + else if (PanelState.bHasCoachGuidance) + { + PanelState.Headline = ActiveCoachBrief.Headline; + PanelState.SecondaryLine = ActiveCoachBrief.PrimaryActionLabel; + PanelState.PrimaryActionLabel = ActiveCoachBrief.PrimaryActionLabel; + PanelState.FocusDeckId = ActiveCoachBrief.FocusDeckId; + PanelState.FocusMethodSegmentId = ActiveCoachBrief.FocusMethodSegmentId; + PanelState.FocusCaseCount = ActiveCoachBrief.FocusCaseIds.Num(); + PanelState.SuggestedMode = ActiveCoachBrief.SuggestedMode; + PanelState.SuggestedSelectionPolicy = ActiveCoachBrief.SuggestedSelectionPolicy; + } + else if (PanelState.bHasFollowUpGuidance) + { + PanelState.Headline = ActiveCoachFollowUpBrief.Headline; + PanelState.SecondaryLine = ActiveCoachFollowUpBrief.PrimaryActionLabel; + PanelState.PrimaryActionLabel = ActiveCoachFollowUpBrief.PrimaryActionLabel; + PanelState.FocusDeckId = ActiveCoachFollowUpBrief.FocusDeckId; + PanelState.FocusMethodSegmentId = ActiveCoachFollowUpBrief.FocusMethodSegmentId; + PanelState.FocusCaseCount = ActiveCoachFollowUpBrief.FocusCaseIds.Num(); + PanelState.SuggestedMode = ActiveCoachFollowUpBrief.SuggestedMode; + PanelState.SuggestedSelectionPolicy = ActiveCoachFollowUpBrief.SuggestedSelectionPolicy; + } + else if (HasActiveRun()) + { + PanelState.Headline = ActiveRunState.Session.SessionState == EHyperTwistTrainingSessionState::Completed + ? TEXT("Training run is complete") + : TEXT("Training run is active"); + PanelState.SecondaryLine = ActiveRunState.ActiveDeck.Title.IsEmpty() + ? TEXT("Refresh coaching state to continue.") + : ActiveRunState.ActiveDeck.Title; + } + else + { + PanelState.Headline = TEXT("Training state is stable"); + PanelState.SecondaryLine = TEXT("No immediate coach queue work is active."); + } + + PanelState.QueueStatusLine = PanelState.TotalQueueEntryCount > 0 + ? FString::Printf( + TEXT("Queue: %d total, %d ready, %d deferred"), + PanelState.TotalQueueEntryCount, + PanelState.ReadyQueueEntryCount, + PanelState.DeferredQueueEntryCount + ) + : TEXT("Queue: no queued coach work"); + PanelState.VerificationStatusLine = ActiveRepositoryRoundTripVerification.ResolvedPath.IsEmpty() + ? TEXT("Verification: not run yet") + : (ActiveRepositoryRoundTripVerification.bDidRoundTripPass + ? TEXT("Verification: repository round-trip passed") + : TEXT("Verification: repository round-trip needs attention")); + return PanelState; +} + FHyperTwistTrainingDeckHistorySummary UHyperTwistTrainingSubsystem::GetActiveDeckHistorySummary() const { return ActiveDeckHistorySummary; @@ -1065,6 +1376,99 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachFollowUpRun( return RunState; } +FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartNextQueuedCoachRun( + const FString& SessionId, + int32 MaxCases +) +{ + const FHyperTwistTrainingCoachSessionQueueStateEntry* NextRunnableEntry = + HyperTwistTrainingSubsystemInternal::FindNextRunnableCoachQueueEntry(ActiveCoachSessionQueueState); + if (NextRunnableEntry == nullptr) + { + return FHyperTwistTrainingRunState(); + } + + if (NextRunnableEntry->SourceLabel == TEXT("primary-brief")) + { + return StartCoachRecommendedRun(SessionId, MaxCases); + } + if (NextRunnableEntry->SourceLabel == TEXT("follow-up-brief")) + { + return StartCoachFollowUpRun(SessionId, MaxCases); + } + + const FHyperTwistTrainingDeck QueueDeck = HyperTwistTrainingSubsystemInternal::BuildCoachQueueDeckFromEntry( + *NextRunnableEntry, + ActiveRunState, + MaxCases + ); + if (!QueueDeck.IsStructurallyValid()) + { + return FHyperTwistTrainingRunState(); + } + + const FString UserId = !ActiveCoachSessionQueueState.UserId.IsEmpty() + ? ActiveCoachSessionQueueState.UserId + : (ActiveRunState.Session.UserId.IsEmpty() ? TEXT("local-user") : ActiveRunState.Session.UserId); + const FHyperTwistTrainingCoachSessionQueueState QueueStateBeforeStart = ActiveCoachSessionQueueState; + const FHyperTwistCoachBrief QueueBrief = HyperTwistTrainingSubsystemInternal::BuildCoachBriefFromQueueEntry( + QueueStateBeforeStart, + *NextRunnableEntry, + QueueStateBeforeStart.ReferenceUtc + ); + const FHyperTwistTrainingCoachActionPlan DraftActionPlan = + UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan( + QueueBrief, + FString::Printf(TEXT("coach_queue_%s_%s"), *NextRunnableEntry->EntryId, *SessionId), + FString() + ); + FHyperTwistTrainingRunState RunState = StartTrainingRunFromDeck( + QueueDeck, + UserId, + SessionId, + NextRunnableEntry->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::StartCoachSessionQueueEntryBySourceLabel( + QueueStateBeforeStart, + NextRunnableEntry->SourceLabel, + 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, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h index 81c2ea7..961a11f 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h @@ -53,6 +53,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingCoachSessionQueueExecutionSummary CachedCoachQueueExecutionSummary; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachPanelState CachedCoachPanelState; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingDeck CachedCoachRecommendedDeck; @@ -103,6 +106,9 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingRunState StartCoachFollowUpRun(int32 MaxCases); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingRunState StartNextQueuedCoachRun(int32 MaxCases); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") FHyperTwistTrainingRepositoryRoundTripVerification VerifyTrainingRepositoryRoundTrip(); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index 8851835..1486e62 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -50,6 +50,9 @@ public: UObject* WorldContextObject ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingCoachPanelState GetActiveTrainingCoachPanelState(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingDeckHistorySummary GetActiveTrainingDeckHistorySummary(UObject* WorldContextObject); @@ -414,6 +417,13 @@ public: int32 MaxCases ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingRunState StartNextQueuedCoachRun( + UObject* WorldContextObject, + const FString& SessionId, + int32 MaxCases + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingRunState StartSampleClassicTrainingRun( UObject* WorldContextObject, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h index abcd3d9..03fc328 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h @@ -55,6 +55,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingCoachSessionQueueExecutionSummary CachedCoachQueueExecutionSummary; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachPanelState CachedCoachPanelState; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingDeck CachedCoachRecommendedDeck; @@ -105,6 +108,9 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingRunState StartCoachFollowUpTrainingRun(int32 MaxCases); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingRunState StartNextQueuedCoachTrainingRun(int32 MaxCases); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") FHyperTwistTrainingRepositoryRoundTripVerification VerifyRepositoryRoundTrip(); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index d44575b..6d19b46 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -41,6 +41,9 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingRepositoryRoundTripVerification GetActiveRepositoryRoundTripVerification() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachPanelState GetActiveCoachPanelState() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingDeckHistorySummary GetActiveDeckHistorySummary() const; @@ -333,6 +336,12 @@ public: int32 MaxCases ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingRunState StartNextQueuedCoachRun( + const FString& SessionId, + int32 MaxCases + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") FHyperTwistTrainingRunState StartTrainingRunFromDeck( const FHyperTwistTrainingDeck& Deck, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 4297ce0..6e7d8a0 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -4182,3 +4182,106 @@ struct FHyperTwistTrainingRepositoryRoundTripVerification return !ResolvedPath.IsEmpty() && bSaveSucceeded && bLoadSucceeded; } }; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachPanelState +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SecondaryLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString QueueStatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString VerificationStatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PrimaryActionLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString NextQueueSourceLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActiveQueueEntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SignalCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 FocusCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalQueueEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ReadyQueueEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DeferredQueueEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 FollowUpQueueEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasActiveRun = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasCoachGuidance = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasFollowUpGuidance = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasQueueWork = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasDeferredQueueWork = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasRunnableQueueEntry = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCanStartCoachRecommendedRun = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCanStartCoachFollowUpRun = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCanStartQueuedRun = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRepositoryVerificationPassed = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRepositoryVerificationNeedsAttention = true; + + bool IsStructurallyValid() const + { + return !Headline.IsEmpty() + || bHasCoachGuidance + || bHasFollowUpGuidance + || bHasQueueWork + || bHasRunnableQueueEntry + || bHasActiveRun; + } +};