From a6ca129f4dbdbde4d497be7ba0d0dbae4a0868a5 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Fri, 28 Aug 2026 20:30:50 +0000 Subject: [PATCH] Simplify active story intelligence navigation --- PlotLine.Tests/Program.cs | 40 ++++++++++++++++++- PlotLine/Services/CoreServices.cs | 11 +++-- PlotLine/Services/StoryIntelligenceService.cs | 10 ++++- PlotLine/ViewModels/CoreViewModels.cs | 1 + PlotLine/ViewModels/OnboardingViewModels.cs | 2 + PlotLine/Views/Books/Details.cshtml | 20 ++++++++-- PlotLine/Views/Projects/Index.cshtml | 38 +++++++----------- 7 files changed, 89 insertions(+), 33 deletions(-) diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 3a6a69e..252044d 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -235,7 +235,9 @@ var tests = new (string Name, Action Test)[] ("Story Intelligence global progress uses whole run scene totals", StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals), ("Story Intelligence recovered provider failures stay operational", StoryIntelligenceRecoveredProviderFailuresStayOperational), ("Story Intelligence recovered JSON repair stays operational", StoryIntelligenceRecoveredJsonRepairStaysOperational), - ("Story Intelligence terminal failures remain user visible", StoryIntelligenceTerminalFailuresRemainUserVisible) + ("Story Intelligence terminal failures remain user visible", StoryIntelligenceTerminalFailuresRemainUserVisible), + ("Story Intelligence active progress only appears in global strip", StoryIntelligenceActiveProgressOnlyAppearsInGlobalStrip), + ("Story Intelligence book actions do not open review centre while running", StoryIntelligenceBookActionsDoNotOpenReviewCentreWhileRunning) }; foreach (var test in tests) @@ -432,6 +434,42 @@ static void StoryIntelligenceTerminalFailuresRemainUserVisible() Assert(globalViewModel.Contains("public bool HasFailures => FailedRunCount > 0 || FailedScenes > 0", StringComparison.Ordinal), "Global banner warning state should be driven by terminal persisted failures."); } +static void StoryIntelligenceActiveProgressOnlyAppearsInGlobalStrip() +{ + var root = PlotLineRoot(); + var projectsView = File.ReadAllText(Path.Combine(root, "Views/Projects/Index.cshtml")); + var layout = File.ReadAllText(Path.Combine(root, "Views/Shared/_Layout.cshtml")); + var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs")); + + Assert(!projectsView.Contains("Story Intelligence is reading", StringComparison.Ordinal), "Projects page should not duplicate the active Story Intelligence progress card."); + Assert(!projectsView.Contains("StoryIntelligenceProgress", StringComparison.Ordinal), "Projects page should not provide a second View progress target while the global strip is active."); + Assert(projectsView.Contains("HasPipelineBooks && !Model.StoryIntelligence.HasActiveImport", StringComparison.Ordinal), "Saved pipeline cards should be hidden while active progress is already shown globally."); + Assert(layout.Contains("data-story-intelligence-global-strip", StringComparison.Ordinal), "Active Story Intelligence progress should remain available in the shared page header."); + Assert(controller.Contains("return RedirectToAction(\"Details\", \"Books\", new { id = bookId });", StringComparison.Ordinal), "Starting permanent book analysis should return to the book page."); +} + +static void StoryIntelligenceBookActionsDoNotOpenReviewCentreWhileRunning() +{ + var root = PlotLineRoot(); + var coreViewModels = File.ReadAllText(Path.Combine(root, "ViewModels/CoreViewModels.cs")); + var onboardingViewModels = File.ReadAllText(Path.Combine(root, "ViewModels/OnboardingViewModels.cs")); + var coreServices = File.ReadAllText(Path.Combine(root, "Services/CoreServices.cs")); + var storyService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceService.cs")); + var bookView = File.ReadAllText(Path.Combine(root, "Views/Books/Details.cshtml")); + var projectsView = File.ReadAllText(Path.Combine(root, "Views/Projects/Index.cshtml")); + + Assert(coreViewModels.Contains("public bool CanContinue", StringComparison.Ordinal), "Book action model should distinguish runnable review links from running status."); + Assert(onboardingViewModels.Contains("public bool IsInProgress", StringComparison.Ordinal), "Project dashboard book status should distinguish running analysis from saved review."); + Assert(onboardingViewModels.Contains("public bool CanContinueReview => !IsInProgress", StringComparison.Ordinal), "Project dashboard should suppress review buttons for running analysis."); + Assert(coreServices.Contains("var isInProgress = string.Equals(pipeline.Status, StoryIntelligencePipelineStatuses.InProgress", StringComparison.Ordinal), "Book action builder should inspect the pipeline status."); + Assert(coreServices.Contains("CanContinue = !isInProgress", StringComparison.Ordinal), "Book action builder should block review navigation while analysis is running."); + Assert(coreServices.Contains("ActionLabel = pipeline.IsComplete ? \"Open Review Centre\" : \"Continue review\"", StringComparison.Ordinal), "Open Review Centre should be reserved for complete Story Intelligence."); + Assert(storyService.Contains("IsInProgress = string.Equals(state.Status, StoryIntelligencePipelineStatuses.InProgress", StringComparison.Ordinal), "Project dashboard pipeline rows should map in-progress state."); + Assert(bookView.Contains("if (action.CanContinue)", StringComparison.Ordinal), "Book details should render the review link only when the pipeline can continue."); + Assert(bookView.Contains("Analysis running", StringComparison.Ordinal), "Book details should show running status instead of a premature review-centre button."); + Assert(projectsView.Contains("primaryPipelineBook.CanContinueReview", StringComparison.Ordinal), "Projects page should only render review actions for continuable pipeline books."); +} + static void StoryIntelligenceReviewCentreRouteReplacesLinearReviewResume() { var root = PlotLineRoot(); diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index 91382b7..15a57fa 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -1372,6 +1372,7 @@ public sealed class BookService( { HasCanonicalSceneSources = hasCanonicalSceneSources, CanStart = hasCanonicalSceneSources, + CanContinue = false, StatusLabel = hasCanonicalSceneSources ? "Not analysed" : "Core import required", ActionLabel = "Analyse My Book", Description = hasCanonicalSceneSources @@ -1380,15 +1381,19 @@ public sealed class BookService( }; } + var isInProgress = string.Equals(pipeline.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase); return new StoryIntelligenceBookActionViewModel { HasCanonicalSceneSources = hasCanonicalSceneSources, CanStart = false, - StatusLabel = pipeline.IsComplete ? "Complete" : pipeline.Status, - ActionLabel = "Open Review Centre", + CanContinue = !isInProgress, + StatusLabel = pipeline.IsComplete ? "Complete" : isInProgress ? "Analysis in progress" : pipeline.Status, + ActionLabel = pipeline.IsComplete ? "Open Review Centre" : "Continue review", Description = pipeline.IsComplete ? "Story Intelligence review is saved for this book." - : "Story Intelligence has saved review categories for this book." + : isInProgress + ? "Story Intelligence is analysing this book in the background." + : "Story Intelligence has saved review categories for this book." }; } diff --git a/PlotLine/Services/StoryIntelligenceService.cs b/PlotLine/Services/StoryIntelligenceService.cs index 56e79e1..9d46b55 100644 --- a/PlotLine/Services/StoryIntelligenceService.cs +++ b/PlotLine/Services/StoryIntelligenceService.cs @@ -257,7 +257,8 @@ public sealed class StoryIntelligenceService( BookTitle = state.BookDisplayTitle, StatusLabel = PipelineStatusLabel(state), Description = PipelineDescription(state), - IsComplete = state.IsComplete + IsComplete = state.IsComplete, + IsInProgress = string.Equals(state.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase) }; private static string PipelineStatusLabel(StoryIntelligenceBookPipelineState state) @@ -267,6 +268,11 @@ public sealed class StoryIntelligenceService( return "Story Intelligence Review Saved"; } + if (string.Equals(state.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase)) + { + return "Story Intelligence In Progress"; + } + return state.CurrentReviewStage switch { StoryIntelligencePipelineStages.KnowledgeReview => "Review Centre", @@ -288,6 +294,8 @@ public sealed class StoryIntelligenceService( private static string PipelineDescription(StoryIntelligenceBookPipelineState state) => state.IsComplete ? "Story Intelligence review is saved." + : string.Equals(state.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase) + ? "Story Intelligence is analysing this book." : state.CurrentReviewStage switch { StoryIntelligencePipelineStages.KnowledgeReview => "Open the Review Centre and choose which saved category to handle.", diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index ca25ebc..7fbf992 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -263,6 +263,7 @@ public sealed class StoryIntelligenceBookActionViewModel { public bool HasCanonicalSceneSources { get; set; } public bool CanStart { get; set; } + public bool CanContinue { get; set; } public string StatusLabel { get; set; } = "Not analysed"; public string ActionLabel { get; set; } = "Analyse My Book"; public string Description { get; set; } = "Analyse this book from its saved chapters and scenes."; diff --git a/PlotLine/ViewModels/OnboardingViewModels.cs b/PlotLine/ViewModels/OnboardingViewModels.cs index 8eb6a0f..686cefa 100644 --- a/PlotLine/ViewModels/OnboardingViewModels.cs +++ b/PlotLine/ViewModels/OnboardingViewModels.cs @@ -718,6 +718,8 @@ public sealed class StoryIntelligencePipelineBookStatusViewModel public string StatusLabel { get; init; } = "Story Intelligence Needs Review"; public string Description { get; init; } = string.Empty; public bool IsComplete { get; init; } + public bool IsInProgress { get; init; } + public bool CanContinueReview => !IsInProgress; } public sealed class CompanionPresenceViewModel diff --git a/PlotLine/Views/Books/Details.cshtml b/PlotLine/Views/Books/Details.cshtml index 6a4cb65..53d0b05 100644 --- a/PlotLine/Views/Books/Details.cshtml +++ b/PlotLine/Views/Books/Details.cshtml @@ -81,10 +81,17 @@ } else { - @action.ActionLabel + @if (action.CanContinue) + { + @action.ActionLabel + } + else + { + Analysis running + } } @@ -212,6 +219,11 @@ return "Story Intelligence review is saved for this book."; } + if (string.Equals(pipeline.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase)) + { + return "Story Intelligence is analysing this book in the background."; + } + return pipeline.CurrentReviewStage switch { StoryIntelligencePipelineStages.NarrativeReview => "Open the Review Centre and choose which saved category to handle.", diff --git a/PlotLine/Views/Projects/Index.cshtml b/PlotLine/Views/Projects/Index.cshtml index 3c7b174..06a1c5b 100644 --- a/PlotLine/Views/Projects/Index.cshtml +++ b/PlotLine/Views/Projects/Index.cshtml @@ -34,27 +34,10 @@ } -@if (Model.StoryIntelligence.HasActiveImport) +@if (Model.StoryIntelligence.HasPipelineBooks && !Model.StoryIntelligence.HasActiveImport) { -
-
-

Manuscript import

-

Story Intelligence is reading

-

- @(Model.StoryIntelligence.ActiveImportBookTitle ?? "Your manuscript") - Progress: @(Model.StoryIntelligence.ActiveImportProgressLabel ?? "In progress") -

-
- View progress -
-} -else if (Model.StoryIntelligence.HasPipelineBooks) -{ - var primaryPipelineBook = Model.StoryIntelligence.PipelineBooks.FirstOrDefault(item => !item.IsComplete) + var primaryPipelineBook = Model.StoryIntelligence.PipelineBooks.FirstOrDefault(item => item.CanContinueReview && !item.IsComplete) + ?? Model.StoryIntelligence.PipelineBooks.FirstOrDefault(item => item.CanContinueReview) ?? Model.StoryIntelligence.PipelineBooks.First();
@@ -72,10 +55,17 @@ else if (Model.StoryIntelligence.HasPipelineBooks) } - @Model.StoryIntelligence.ButtonText + @if (primaryPipelineBook.CanContinueReview) + { + @Model.StoryIntelligence.ButtonText + } + else + { + Analysis running + }
}