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 global progress uses whole run scene totals", StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals),
("Story Intelligence recovered provider failures stay operational", StoryIntelligenceRecoveredProviderFailuresStayOperational), ("Story Intelligence recovered provider failures stay operational", StoryIntelligenceRecoveredProviderFailuresStayOperational),
("Story Intelligence recovered JSON repair stays operational", StoryIntelligenceRecoveredJsonRepairStaysOperational), ("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) 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."); 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() static void StoryIntelligenceReviewCentreRouteReplacesLinearReviewResume()
{ {
var root = PlotLineRoot(); var root = PlotLineRoot();

View File

@ -1372,6 +1372,7 @@ public sealed class BookService(
{ {
HasCanonicalSceneSources = hasCanonicalSceneSources, HasCanonicalSceneSources = hasCanonicalSceneSources,
CanStart = hasCanonicalSceneSources, CanStart = hasCanonicalSceneSources,
CanContinue = false,
StatusLabel = hasCanonicalSceneSources ? "Not analysed" : "Core import required", StatusLabel = hasCanonicalSceneSources ? "Not analysed" : "Core import required",
ActionLabel = "Analyse My Book", ActionLabel = "Analyse My Book",
Description = hasCanonicalSceneSources Description = hasCanonicalSceneSources
@ -1380,14 +1381,18 @@ public sealed class BookService(
}; };
} }
var isInProgress = string.Equals(pipeline.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase);
return new StoryIntelligenceBookActionViewModel return new StoryIntelligenceBookActionViewModel
{ {
HasCanonicalSceneSources = hasCanonicalSceneSources, HasCanonicalSceneSources = hasCanonicalSceneSources,
CanStart = false, CanStart = false,
StatusLabel = pipeline.IsComplete ? "Complete" : pipeline.Status, CanContinue = !isInProgress,
ActionLabel = "Open Review Centre", StatusLabel = pipeline.IsComplete ? "Complete" : isInProgress ? "Analysis in progress" : pipeline.Status,
ActionLabel = pipeline.IsComplete ? "Open Review Centre" : "Continue review",
Description = pipeline.IsComplete Description = pipeline.IsComplete
? "Story Intelligence review is saved for this book." ? "Story Intelligence review is saved for this book."
: isInProgress
? "Story Intelligence is analysing this book in the background."
: "Story Intelligence has saved review categories for this book." : "Story Intelligence has saved review categories for this book."
}; };
} }

View File

@ -257,7 +257,8 @@ public sealed class StoryIntelligenceService(
BookTitle = state.BookDisplayTitle, BookTitle = state.BookDisplayTitle,
StatusLabel = PipelineStatusLabel(state), StatusLabel = PipelineStatusLabel(state),
Description = PipelineDescription(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) private static string PipelineStatusLabel(StoryIntelligenceBookPipelineState state)
@ -267,6 +268,11 @@ public sealed class StoryIntelligenceService(
return "Story Intelligence Review Saved"; return "Story Intelligence Review Saved";
} }
if (string.Equals(state.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase))
{
return "Story Intelligence In Progress";
}
return state.CurrentReviewStage switch return state.CurrentReviewStage switch
{ {
StoryIntelligencePipelineStages.KnowledgeReview => "Review Centre", StoryIntelligencePipelineStages.KnowledgeReview => "Review Centre",
@ -288,6 +294,8 @@ public sealed class StoryIntelligenceService(
private static string PipelineDescription(StoryIntelligenceBookPipelineState state) private static string PipelineDescription(StoryIntelligenceBookPipelineState state)
=> state.IsComplete => state.IsComplete
? "Story Intelligence review is saved." ? "Story Intelligence review is saved."
: string.Equals(state.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase)
? "Story Intelligence is analysing this book."
: state.CurrentReviewStage switch : state.CurrentReviewStage switch
{ {
StoryIntelligencePipelineStages.KnowledgeReview => "Open the Review Centre and choose which saved category to handle.", 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 HasCanonicalSceneSources { get; set; }
public bool CanStart { get; set; } public bool CanStart { get; set; }
public bool CanContinue { get; set; }
public string StatusLabel { get; set; } = "Not analysed"; public string StatusLabel { get; set; } = "Not analysed";
public string ActionLabel { get; set; } = "Analyse My Book"; public string ActionLabel { get; set; } = "Analyse My Book";
public string Description { get; set; } = "Analyse this book from its saved chapters and scenes."; 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 StatusLabel { get; init; } = "Story Intelligence Needs Review";
public string Description { get; init; } = string.Empty; public string Description { get; init; } = string.Empty;
public bool IsComplete { get; init; } public bool IsComplete { get; init; }
public bool IsInProgress { get; init; }
public bool CanContinueReview => !IsInProgress;
} }
public sealed class CompanionPresenceViewModel public sealed class CompanionPresenceViewModel

View File

@ -80,12 +80,19 @@
</form> </form>
} }
else else
{
@if (action.CanContinue)
{ {
<a class="btn btn-primary" <a class="btn btn-primary"
asp-controller="Onboarding" asp-controller="Onboarding"
asp-action="ContinueStoryIntelligence" asp-action="ContinueStoryIntelligence"
asp-route-bookId="@Model.Book.BookID">@action.ActionLabel</a> asp-route-bookId="@Model.Book.BookID">@action.ActionLabel</a>
} }
else
{
<span class="status-pill">Analysis running</span>
}
}
</div> </div>
</section> </section>
} }
@ -212,6 +219,11 @@
return "Story Intelligence review is saved for this book."; 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 return pipeline.CurrentReviewStage switch
{ {
StoryIntelligencePipelineStages.NarrativeReview => "Open the Review Centre and choose which saved category to handle.", StoryIntelligencePipelineStages.NarrativeReview => "Open the Review Centre and choose which saved category to handle.",

View File

@ -34,27 +34,10 @@
</section> </section>
} }
@if (Model.StoryIntelligence.HasActiveImport) @if (Model.StoryIntelligence.HasPipelineBooks && !Model.StoryIntelligence.HasActiveImport)
{ {
<section class="story-intelligence-dashboard-card story-intelligence-dashboard-card--compact" var primaryPipelineBook = Model.StoryIntelligence.PipelineBooks.FirstOrDefault(item => item.CanContinueReview && !item.IsComplete)
aria-label="Active manuscript import"> ?? Model.StoryIntelligence.PipelineBooks.FirstOrDefault(item => item.CanContinueReview)
<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)
?? Model.StoryIntelligence.PipelineBooks.First(); ?? Model.StoryIntelligence.PipelineBooks.First();
<section class="story-intelligence-dashboard-card story-intelligence-dashboard-card--compact" <section class="story-intelligence-dashboard-card story-intelligence-dashboard-card--compact"
aria-label="Story Intelligence status"> aria-label="Story Intelligence status">
@ -72,10 +55,17 @@ else if (Model.StoryIntelligence.HasPipelineBooks)
} }
</div> </div>
</div> </div>
@if (primaryPipelineBook.CanContinueReview)
{
<a class="btn btn-primary" <a class="btn btn-primary"
asp-controller="Onboarding" asp-controller="Onboarding"
asp-action="ContinueStoryIntelligence" asp-action="ContinueStoryIntelligence"
asp-route-bookId="@primaryPipelineBook.BookID">@Model.StoryIntelligence.ButtonText</a> asp-route-bookId="@primaryPipelineBook.BookID">@Model.StoryIntelligence.ButtonText</a>
}
else
{
<span class="status-pill">Analysis running</span>
}
</section> </section>
} }