Simplify active story intelligence navigation

This commit is contained in:
Nick Beckley 2026-08-28 20:30:50 +00:00
parent a319df2455
commit a6ca129f4d
7 changed files with 89 additions and 33 deletions

View File

@ -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("<span class=\"status-pill\">Analysis running</span>", 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();

View File

@ -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."
};
}

View File

@ -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.",

View File

@ -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.";

View File

@ -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

View File

@ -81,10 +81,17 @@
}
else
{
<a class="btn btn-primary"
asp-controller="Onboarding"
asp-action="ContinueStoryIntelligence"
asp-route-bookId="@Model.Book.BookID">@action.ActionLabel</a>
@if (action.CanContinue)
{
<a class="btn btn-primary"
asp-controller="Onboarding"
asp-action="ContinueStoryIntelligence"
asp-route-bookId="@Model.Book.BookID">@action.ActionLabel</a>
}
else
{
<span class="status-pill">Analysis running</span>
}
}
</div>
</section>
@ -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.",

View File

@ -34,27 +34,10 @@
</section>
}
@if (Model.StoryIntelligence.HasActiveImport)
@if (Model.StoryIntelligence.HasPipelineBooks && !Model.StoryIntelligence.HasActiveImport)
{
<section class="story-intelligence-dashboard-card story-intelligence-dashboard-card--compact"
aria-label="Active manuscript import">
<div>
<p class="eyebrow">Manuscript import</p>
<h2>Story Intelligence is reading</h2>
<p>
<strong>@(Model.StoryIntelligence.ActiveImportBookTitle ?? "Your manuscript")</strong>
<span class="d-block">Progress: @(Model.StoryIntelligence.ActiveImportProgressLabel ?? "In progress")</span>
</p>
</div>
<a class="btn btn-primary"
asp-controller="Onboarding"
asp-action="StoryIntelligenceProgress"
asp-route-batchId="@Model.StoryIntelligence.ActiveBatchID">View progress</a>
</section>
}
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();
<section class="story-intelligence-dashboard-card story-intelligence-dashboard-card--compact"
aria-label="Story Intelligence status">
@ -72,10 +55,17 @@ else if (Model.StoryIntelligence.HasPipelineBooks)
}
</div>
</div>
<a class="btn btn-primary"
asp-controller="Onboarding"
asp-action="ContinueStoryIntelligence"
asp-route-bookId="@primaryPipelineBook.BookID">@Model.StoryIntelligence.ButtonText</a>
@if (primaryPipelineBook.CanContinueReview)
{
<a class="btn btn-primary"
asp-controller="Onboarding"
asp-action="ContinueStoryIntelligence"
asp-route-bookId="@primaryPipelineBook.BookID">@Model.StoryIntelligence.ButtonText</a>
}
else
{
<span class="status-pill">Analysis running</span>
}
</section>
}