From 0ec0167746333fcc27e3f08b25eecd8a0405bd2d Mon Sep 17 00:00:00 2001
From: Nick Beckley
Date: Sat, 29 Aug 2026 09:33:14 +0000
Subject: [PATCH] Remove obsolete Story Intelligence scene review workflow
---
PlotLine.Tests/Program.cs | 72 +++-
PlotLine/Controllers/OnboardingController.cs | 129 +++----
PlotLine/Services/CoreServices.cs | 10 +-
.../OnboardingStoryIntelligenceService.cs | 36 +-
.../StoryIntelligenceAssetImportService.cs | 13 +-
...StoryIntelligenceCharacterImportService.cs | 13 +-
.../StoryIntelligenceImportCommitService.cs | 2 +-
...StoryIntelligenceKnowledgeImportService.cs | 13 +-
.../StoryIntelligenceLocationImportService.cs | 13 +-
...StoryIntelligenceNarrativeReviewService.cs | 19 +-
...ryIntelligenceRelationshipImportService.cs | 13 +-
PlotLine/Services/StoryIntelligenceService.cs | 4 +-
PlotLine/ViewModels/OnboardingViewModels.cs | 4 +
PlotLine/Views/Books/Details.cshtml | 4 +-
.../Views/Onboarding/BuildComplete.cshtml | 8 +-
.../Views/Onboarding/StoryIntelligence.cshtml | 4 +-
.../StoryIntelligenceContext.cshtml | 2 +-
.../StoryIntelligenceProgress.cshtml | 4 +-
.../Onboarding/StoryIntelligenceReview.cshtml | 326 ------------------
.../StoryIntelligenceReviewCentre.cshtml | 24 +-
20 files changed, 250 insertions(+), 463 deletions(-)
delete mode 100644 PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml
diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs
index 5f26847..d922360 100644
--- a/PlotLine.Tests/Program.cs
+++ b/PlotLine.Tests/Program.cs
@@ -231,8 +231,11 @@ var tests = new (string Name, Action Test)[]
("Word manuscript unlink keeps Story Intelligence data untouched", WordManuscriptUnlinkKeepsStoryIntelligenceDataUntouched),
("Word manuscript cleanup remains relinkable and idempotent", WordManuscriptCleanupRemainsRelinkableAndIdempotent),
("Story Intelligence Review Centre route replaces linear review resume", StoryIntelligenceReviewCentreRouteReplacesLinearReviewResume),
+ ("Story Intelligence obsolete create scene workflow is removed", StoryIntelligenceObsoleteCreateSceneWorkflowIsRemoved),
("Story Intelligence Review Centre counts only needs-attention categories", StoryIntelligenceReviewCentreCountsOnlyNeedsAttentionCategories),
+ ("Story Intelligence Review Centre opens with character gate", StoryIntelligenceReviewCentreOpensWithCharacterGate),
("Story Intelligence review modules are independent after character gate", StoryIntelligenceReviewModulesAreIndependentAfterCharacterGate),
+ ("Story Intelligence downstream review uses completed scene analysis", StoryIntelligenceDownstreamReviewUsesCompletedSceneAnalysis),
("Story Intelligence review posts do not use success interstitials", StoryIntelligenceReviewPostsDoNotUseSuccessInterstitials),
("Story Intelligence review forms share busy submit guard", StoryIntelligenceReviewFormsShareBusySubmitGuard),
("Story Intelligence heavy review modules offer bounded rendering", StoryIntelligenceHeavyReviewModulesOfferBoundedRendering),
@@ -609,7 +612,7 @@ static void StoryIntelligenceBookActionsDoNotOpenReviewCentreWhileRunning()
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(coreServices.Contains("ActionLabel = isInProgress ? \"Analysis running\" : \"Open Review Centre\"", StringComparison.Ordinal), "Open Review Centre should be available for completed analysis that still has review work.");
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.");
@@ -626,8 +629,24 @@ static void StoryIntelligenceReviewCentreRouteReplacesLinearReviewResume()
Assert(controller.Contains("story-intelligence/review-centre", StringComparison.Ordinal), "Review Centre route should exist.");
Assert(controller.Contains("StoryIntelligenceResumeRoutes.ReviewCentre => RedirectToAction(nameof(StoryIntelligenceReviewCentre)", StringComparison.Ordinal), "Resume routing should open Review Centre.");
Assert(service.Contains("public const string ReviewCentre = \"ReviewCentre\";", StringComparison.Ordinal), "Resume route constants should include ReviewCentre.");
- Assert(service.Contains("? StoryIntelligenceResumeRoutes.ReviewCentre", StringComparison.Ordinal), "Completed character resolution should resume to Review Centre.");
- Assert(view.Contains("Review only the story intelligence that needs an author decision.", StringComparison.Ordinal), "Review Centre should describe needs-attention review.");
+ Assert(service.Contains("SceneAnalysisComplete(state)\n ? StoryIntelligenceResumeRoutes.ReviewCentre", StringComparison.Ordinal), "Completed scene analysis should resume to Review Centre.");
+ Assert(view.Contains("Your manuscript analysis is complete.", StringComparison.Ordinal), "Review Centre should describe completed analysis.");
+}
+
+static void StoryIntelligenceObsoleteCreateSceneWorkflowIsRemoved()
+{
+ var root = PlotLineRoot();
+ var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs"));
+ var progressView = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceProgress.cshtml"));
+ var overviewView = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligence.cshtml"));
+ var reviewViewPath = Path.Combine(root, "Views/Onboarding/StoryIntelligenceReview.cshtml");
+ var legacyReviewAction = ExtractBetween(controller, "public async Task StoryIntelligenceReview", "[HttpGet(\"story-intelligence/review-centre\")]");
+
+ Assert(!File.Exists(reviewViewPath), "Obsolete Create Scene Intelligence view should be removed.");
+ Assert(legacyReviewAction.Contains("RedirectToAction(nameof(StoryIntelligenceReviewCentre)", StringComparison.Ordinal), "Legacy review URL should redirect to Review Centre after analysis.");
+ Assert(!legacyReviewAction.Contains("View(model)", StringComparison.Ordinal), "Legacy review URL must never render obsolete UI.");
+ Assert(!progressView.Contains("Create scene intelligence", StringComparison.OrdinalIgnoreCase), "Progress view should not link to old create scene workflow.");
+ Assert(!overviewView.Contains("Create scene intelligence", StringComparison.OrdinalIgnoreCase), "Overview should not describe old create scene workflow.");
}
static void StoryIntelligenceReviewCentreCountsOnlyNeedsAttentionCategories()
@@ -640,10 +659,23 @@ static void StoryIntelligenceReviewCentreCountsOnlyNeedsAttentionCategories()
Assert(viewModel.Contains("CharacterNeedsAttentionCount => CharacterReview.Candidates.Count", StringComparison.Ordinal), "Character count should be derived from unresolved candidates.");
Assert(viewModel.Contains("LocationNeedsAttentionCount => LocationReview.Candidates.Count", StringComparison.Ordinal), "Location count should be derived from unresolved candidates.");
Assert(viewModel.Contains("NarrativeNeedsAttentionCount => NarrativeReview.Candidates.Count", StringComparison.Ordinal), "Narrative count should be derived from unresolved candidates.");
- Assert(centre.Contains("Scene Metrics", StringComparison.Ordinal) && centre.Contains("0 need attention", StringComparison.Ordinal), "Metrics should be available, not mandatory review.");
+ Assert(centre.Contains("Scene Metrics", StringComparison.Ordinal) && centre.Contains("TotalAnalysedScenes", StringComparison.Ordinal), "Metrics should report analysed scene count.");
Assert(!summary.Contains("Locations identified", StringComparison.Ordinal), "Summary should not present total identified rows as review workload.");
}
+static void StoryIntelligenceReviewCentreOpensWithCharacterGate()
+{
+ var root = PlotLineRoot();
+ var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs"));
+ var centreAction = ExtractBetween(controller, "public async Task StoryIntelligenceReviewCentre", "[HttpGet(\"story-intelligence/characters\")]");
+ var centreView = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceReviewCentre.cshtml"));
+
+ Assert(centreAction.Contains("return View(model);", StringComparison.Ordinal), "Review Centre should render after completed analysis.");
+ Assert(!centreAction.Contains("StoryIntelligenceCharacters", StringComparison.Ordinal), "Review Centre should not redirect unresolved Characters away from the centre.");
+ Assert(centreView.Contains("Resolve Characters first", StringComparison.Ordinal), "Locked modules should explain the character gate.");
+ Assert(centreView.Contains("Locked until Characters are resolved", StringComparison.Ordinal), "Dependent modules should remain visible but locked.");
+}
+
static void StoryIntelligenceReviewModulesAreIndependentAfterCharacterGate()
{
var root = PlotLineRoot();
@@ -658,7 +690,30 @@ static void StoryIntelligenceReviewModulesAreIndependentAfterCharacterGate()
Assert(!relationshipsAction.Contains("LocationStageComplete", StringComparison.Ordinal) && !relationshipsAction.Contains("AssetStageComplete", StringComparison.Ordinal), "Relationships should not require completed location or asset review.");
Assert(!knowledgeAction.Contains("LocationStageComplete", StringComparison.Ordinal) && !knowledgeAction.Contains("RelationshipStageComplete", StringComparison.Ordinal), "Knowledge should not require earlier non-character reviews.");
Assert(!narrativeAction.Contains("KnowledgeStageComplete", StringComparison.Ordinal) && !narrativeAction.Contains("RelationshipStageComplete", StringComparison.Ordinal), "Narrative review should not require knowledge or relationship review.");
- Assert(locationsAction.Contains("CharacterStageComplete", StringComparison.Ordinal), "Dependent review modules should still respect the character gate.");
+ Assert(locationsAction.Contains("CharacterGateSatisfied", StringComparison.Ordinal), "Dependent review modules should still respect the character gate.");
+ Assert(locationsAction.Contains("RedirectToReviewCentreWithCharacterGate", StringComparison.Ordinal), "Direct dependent module URLs should return to Review Centre when the character gate is closed.");
+}
+
+static void StoryIntelligenceDownstreamReviewUsesCompletedSceneAnalysis()
+{
+ var root = PlotLineRoot();
+ var reviewServices = new[]
+ {
+ "StoryIntelligenceCharacterImportService.cs",
+ "StoryIntelligenceLocationImportService.cs",
+ "StoryIntelligenceAssetImportService.cs",
+ "StoryIntelligenceRelationshipImportService.cs",
+ "StoryIntelligenceKnowledgeImportService.cs",
+ "StoryIntelligenceNarrativeReviewService.cs"
+ };
+
+ foreach (var fileName in reviewServices)
+ {
+ var service = File.ReadAllText(Path.Combine(root, "Services", fileName));
+ Assert(!service.Contains("GetImportCommitAsync", StringComparison.Ordinal), $"{fileName} should not require legacy StoryIntelligenceImportCommits.");
+ Assert(service.Contains("StoryIntelligenceRunStatuses.Completed", StringComparison.Ordinal), $"{fileName} should use completed persisted runs as its scene-analysis prerequisite.");
+ Assert(service.Contains("ListSceneResultsAsync", StringComparison.Ordinal), $"{fileName} should read persisted scene analysis results.");
+ }
}
static void StoryIntelligenceReviewPostsDoNotUseSuccessInterstitials()
@@ -2946,7 +3001,6 @@ static void StoryIntelligenceReviewUsesCanonicalStructuralSummariesAndCounts()
var onboardingStory = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs"));
var commit = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceImportCommitService.cs"));
var viewModel = File.ReadAllText(Path.Combine(root, "ViewModels/OnboardingViewModels.cs"));
- var reviewView = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceReview.cshtml"));
var repository = File.ReadAllText(Path.Combine(root, "Data/StoryIntelligenceResultRepository.cs"));
Assert(commit.Contains("BuildReviewScenes(canonicalScenes, sceneResults, importScenes)", StringComparison.Ordinal), "Review confirmation should build canonical scene previews.");
@@ -2956,9 +3010,7 @@ static void StoryIntelligenceReviewUsesCanonicalStructuralSummariesAndCounts()
Assert(commit.Contains("GroupBy(result => result.SceneID!.Value)", StringComparison.Ordinal), "Duplicate mapped SI rows should collapse to one canonical scene preview.");
Assert(onboardingStory.Contains("FirstConfigured(scene.Summary, scene.ParsedScene.Summary?.Short", StringComparison.Ordinal), "Onboarding preview should prefer canonical summary with legacy SI fallback.");
Assert(viewModel.Contains("chapter.Scenes.Count > 0 ? chapter.Scenes.Count : chapter.TotalDetectedScenes", StringComparison.Ordinal), "Review headline total should derive from canonical scene previews when available.");
- Assert(reviewView.Contains("chapter.Scenes.Count > 0 ? chapter.Scenes.Count : chapter.TotalDetectedScenes", StringComparison.Ordinal), "Per-chapter detected count should derive from canonical scene previews.");
- Assert(reviewView.Contains("chapter.Scenes.Count(scene => !scene.HasWarnings)", StringComparison.Ordinal), "Ready count should not include missing or failed canonical scene analysis.");
- Assert(reviewView.Contains("@scene.AnalysisStatus", StringComparison.Ordinal), "Review UI should show missing/failed analysis state on canonical scenes.");
+ Assert(viewModel.Contains("public int TotalAnalysedScenes => Math.Max(TotalCompletedScenes, PipelineDashboard.ScenesImported)", StringComparison.Ordinal), "Review Centre should use analysed canonical scene totals after the old scene-creation view is removed.");
Assert(repository.Contains("SceneID = item.SceneID ??", StringComparison.Ordinal), "Commit should target canonical SceneID when one exists.");
}
@@ -5015,7 +5067,7 @@ static void StoryIntelligenceNarrativeStageFollowsKnowledge()
Assert(pipeline.Contains("RecordNarrativeImportAsync", StringComparison.Ordinal), "Narrative import should mark the pipeline complete.");
Assert(pipelineConstraint.Contains("N'NarrativeReview'", StringComparison.Ordinal) && pipelineConstraint.Contains("N'NarrativeImport'", StringComparison.Ordinal), "Database pipeline constraint should allow narrative stages.");
Assert(knowledgeImport.Contains("hasUndecidedCandidates", StringComparison.Ordinal), "Knowledge review should be able to advance when prior decisions already imported every candidate.");
- Assert(onboarding.Contains("StoryIntelligenceResumeRoutes.Narrative", StringComparison.Ordinal), "Resume logic should route to narrative review.");
+ Assert(onboarding.Contains("SceneAnalysisComplete(state)\n ? StoryIntelligenceResumeRoutes.ReviewCentre", StringComparison.Ordinal), "Resume logic should route completed analysis to Review Centre.");
Assert(controller.Contains("StoryIntelligenceNarrative", StringComparison.Ordinal), "Onboarding controller should expose the narrative review stage.");
Assert(knowledgeComplete.Contains("Review Plot Lines & Threads", StringComparison.Ordinal), "Knowledge completion should lead to narrative review.");
}
diff --git a/PlotLine/Controllers/OnboardingController.cs b/PlotLine/Controllers/OnboardingController.cs
index 108c1f8..f6d06fd 100644
--- a/PlotLine/Controllers/OnboardingController.cs
+++ b/PlotLine/Controllers/OnboardingController.cs
@@ -117,13 +117,13 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
{
StoryIntelligenceResumeRoutes.ReviewCentre => RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = target.BatchID }),
StoryIntelligenceResumeRoutes.Complete => RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = target.BatchID }),
- StoryIntelligenceResumeRoutes.Narrative => RedirectToAction(nameof(StoryIntelligenceNarrative), new { batchId = target.BatchID }),
- StoryIntelligenceResumeRoutes.Knowledge => RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = target.BatchID }),
- StoryIntelligenceResumeRoutes.Relationships => RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId = target.BatchID }),
- StoryIntelligenceResumeRoutes.Assets => RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId = target.BatchID }),
- StoryIntelligenceResumeRoutes.Locations => RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = target.BatchID }),
- StoryIntelligenceResumeRoutes.Characters => RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId = target.BatchID }),
- _ => RedirectToAction(nameof(StoryIntelligenceReview), new { batchId = target.BatchID })
+ StoryIntelligenceResumeRoutes.Narrative => RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = target.BatchID }),
+ StoryIntelligenceResumeRoutes.Knowledge => RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = target.BatchID }),
+ StoryIntelligenceResumeRoutes.Relationships => RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = target.BatchID }),
+ StoryIntelligenceResumeRoutes.Assets => RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = target.BatchID }),
+ StoryIntelligenceResumeRoutes.Locations => RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = target.BatchID }),
+ StoryIntelligenceResumeRoutes.Characters => RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = target.BatchID }),
+ _ => RedirectToAction("Details", "Books", new { id = bookId })
};
}
@@ -192,7 +192,14 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
public async Task StoryIntelligenceProgress(Guid batchId)
{
var model = await storyIntelligence.GetProgressAsync(batchId);
- return model is null ? NotFound() : View(model);
+ if (model is null)
+ {
+ return NotFound();
+ }
+
+ return model.SceneAnalysisComplete
+ ? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId })
+ : RedirectToBook(model);
}
[HttpGet("story-intelligence/review")]
@@ -206,12 +213,12 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
if (model.HasActiveRuns)
{
- return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
+ return RedirectToBook(model);
}
- return model.AllCommitted
- ? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
- : View(model);
+ return model.SceneAnalysisComplete
+ ? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId })
+ : RedirectToBook(model);
}
[HttpGet("story-intelligence/review-centre")]
@@ -225,17 +232,15 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
if (model.HasActiveRuns)
{
- return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
+ return RedirectToBook(model);
}
- if (!model.AllCommitted)
+ if (!model.SceneAnalysisComplete)
{
- return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
+ return RedirectToBook(model);
}
- return !model.PipelineDashboard.CharacterStageComplete
- ? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
- : View(model);
+ return View(model);
}
[HttpGet("story-intelligence/characters")]
@@ -249,15 +254,15 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
if (model.HasActiveRuns)
{
- return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
+ return RedirectToBook(model);
}
- if (!model.AllCommitted)
+ if (!model.SceneAnalysisComplete)
{
- return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
+ return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
}
- return model.PipelineDashboard.CharacterStageComplete && model.CharacterReview.Candidates.Count == 0
+ return model.CharacterGateSatisfied && model.CharacterReview.Candidates.Count == 0
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId })
: View(model);
}
@@ -273,16 +278,16 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
if (model.HasActiveRuns)
{
- return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
+ return RedirectToBook(model);
}
- if (!model.AllCommitted)
+ if (!model.SceneAnalysisComplete)
{
- return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
+ return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
}
- return !model.PipelineDashboard.CharacterStageComplete
- ? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
+ return !model.CharacterGateSatisfied
+ ? RedirectToReviewCentreWithCharacterGate(batchId)
: View(model);
}
@@ -297,17 +302,17 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
if (model.HasActiveRuns)
{
- return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
+ return RedirectToBook(model);
}
- if (!model.AllCommitted)
+ if (!model.SceneAnalysisComplete)
{
- return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
+ return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
}
- if (!model.PipelineDashboard.CharacterStageComplete)
+ if (!model.CharacterGateSatisfied)
{
- return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
+ return RedirectToReviewCentreWithCharacterGate(batchId);
}
return View(model);
@@ -324,17 +329,17 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
if (model.HasActiveRuns)
{
- return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
+ return RedirectToBook(model);
}
- if (!model.AllCommitted)
+ if (!model.SceneAnalysisComplete)
{
- return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
+ return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
}
- if (!model.PipelineDashboard.CharacterStageComplete)
+ if (!model.CharacterGateSatisfied)
{
- return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
+ return RedirectToReviewCentreWithCharacterGate(batchId);
}
return View(model);
@@ -351,7 +356,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
}
TempData["OnboardingStoryIntelligenceMessage"] = "Analysis cancellation requested.";
- return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
+ return RedirectToBook(progress);
}
[HttpPost("story-intelligence/commit")]
@@ -365,14 +370,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
}
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
- if (result.Success && progress.AllCommitted)
- {
- return RedirectToAction(progress.PipelineDashboard.CharacterStageComplete || progress.CharacterReview.Candidates.Count == 0
- ? nameof(StoryIntelligenceReviewCentre)
- : nameof(StoryIntelligenceCharacters), new { batchId });
- }
-
- return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
+ return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
}
[HttpPost("story-intelligence/commit-all")]
@@ -386,11 +384,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
}
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
- return result.Success && progress.AllCommitted
- ? RedirectToAction(progress.PipelineDashboard.CharacterStageComplete || progress.CharacterReview.Candidates.Count == 0
- ? nameof(StoryIntelligenceReviewCentre)
- : nameof(StoryIntelligenceCharacters), new { batchId })
- : RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
+ return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
}
[HttpPost("story-intelligence/characters")]
@@ -496,17 +490,17 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
if (model.HasActiveRuns)
{
- return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
+ return RedirectToBook(model);
}
- if (!model.AllCommitted)
+ if (!model.SceneAnalysisComplete)
{
- return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
+ return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
}
- if (!model.PipelineDashboard.CharacterStageComplete)
+ if (!model.CharacterGateSatisfied)
{
- return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
+ return RedirectToReviewCentreWithCharacterGate(batchId);
}
return View(model);
@@ -546,17 +540,17 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
if (model.HasActiveRuns)
{
- return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
+ return RedirectToBook(model);
}
- if (!model.AllCommitted)
+ if (!model.SceneAnalysisComplete)
{
- return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
+ return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
}
- if (!model.PipelineDashboard.CharacterStageComplete)
+ if (!model.CharacterGateSatisfied)
{
- return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
+ return RedirectToReviewCentreWithCharacterGate(batchId);
}
return View(model);
@@ -594,15 +588,24 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
return NotFound();
}
- if (!progress.PipelineDashboard.CharacterStageComplete)
+ if (!progress.CharacterGateSatisfied)
{
- return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
+ return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
}
var model = await storyIntelligence.GetCompletionAsync(batchId);
return model is null ? NotFound() : View(model);
}
+ private IActionResult RedirectToBook(StoryIntelligenceProgressViewModel model)
+ => RedirectToAction("Details", "Books", new { id = model.BookID });
+
+ private IActionResult RedirectToReviewCentreWithCharacterGate(Guid batchId)
+ {
+ TempData["OnboardingStoryIntelligenceMessage"] = "Resolve Characters before reviewing this category.";
+ return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
+ }
+
[HttpPost("scan-review")]
[RequestSizeLimit(25 * 1024 * 1024)]
[RequestFormLimits(ValueCountLimit = 20000, KeyLengthLimit = 4096, ValueLengthLimit = 1024 * 1024)]
diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs
index 15a57fa..aa4f342 100644
--- a/PlotLine/Services/CoreServices.cs
+++ b/PlotLine/Services/CoreServices.cs
@@ -1382,13 +1382,19 @@ public sealed class BookService(
}
var isInProgress = string.Equals(pipeline.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase);
+ var needsCharacterReview = string.Equals(pipeline.CurrentReviewStage, StoryIntelligencePipelineStages.CharacterReview, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(pipeline.CurrentStage, StoryIntelligencePipelineStages.CharacterReview, StringComparison.OrdinalIgnoreCase);
return new StoryIntelligenceBookActionViewModel
{
HasCanonicalSceneSources = hasCanonicalSceneSources,
CanStart = false,
CanContinue = !isInProgress,
- StatusLabel = pipeline.IsComplete ? "Complete" : isInProgress ? "Analysis in progress" : pipeline.Status,
- ActionLabel = pipeline.IsComplete ? "Open Review Centre" : "Continue review",
+ StatusLabel = pipeline.IsComplete
+ ? "Review complete"
+ : isInProgress
+ ? "Analysis in progress"
+ : needsCharacterReview ? "Character review required" : "Needs review",
+ ActionLabel = isInProgress ? "Analysis running" : "Open Review Centre",
Description = pipeline.IsComplete
? "Story Intelligence review is saved for this book."
: isInProgress
diff --git a/PlotLine/Services/OnboardingStoryIntelligenceService.cs b/PlotLine/Services/OnboardingStoryIntelligenceService.cs
index c7da453..9198193 100644
--- a/PlotLine/Services/OnboardingStoryIntelligenceService.cs
+++ b/PlotLine/Services/OnboardingStoryIntelligenceService.cs
@@ -664,7 +664,8 @@ public sealed class OnboardingStoryIntelligenceService(
PipelineDashboard = new StoryIntelligencePipelineDashboardViewModel
{
ChaptersAnalysed = chapters.Count(chapter => chapter.IsRunComplete),
- ScenesImported = chapters.Sum(chapter => chapter.ScenesCreated),
+ ScenesImported = Math.Max(chapters.Sum(chapter => chapter.ScenesCreated), chapters.Sum(chapter => chapter.CompletedScenes ?? 0)),
+ SceneAnalysisComplete = pipeline is not null && SceneAnalysisComplete(pipeline),
CharactersIdentified = characterReview.Candidates.Count + batch.CharacterDecisions.Count,
CharactersCreatedOrLinked = batch.CharacterDecisions.Count(decision => decision.CreatedOrLinked),
CharacterStageComplete = batch.CharacterStageComplete || characterReview.IsComplete,
@@ -1069,22 +1070,9 @@ public sealed class OnboardingStoryIntelligenceService(
await batchStore.SaveAsync(batch);
- var route = characterStageComplete
+ var route = SceneAnalysisComplete(state)
? StoryIntelligenceResumeRoutes.ReviewCentre
- : state.CurrentStage switch
- {
- StoryIntelligencePipelineStages.RelationshipReview => StoryIntelligenceResumeRoutes.Relationships,
- StoryIntelligencePipelineStages.RelationshipImport => StoryIntelligenceResumeRoutes.Relationships,
- StoryIntelligencePipelineStages.KnowledgeReview => StoryIntelligenceResumeRoutes.Knowledge,
- StoryIntelligencePipelineStages.KnowledgeImport => StoryIntelligenceResumeRoutes.Knowledge,
- StoryIntelligencePipelineStages.NarrativeReview => StoryIntelligenceResumeRoutes.Narrative,
- StoryIntelligencePipelineStages.NarrativeImport => StoryIntelligenceResumeRoutes.Narrative,
- StoryIntelligencePipelineStages.CharacterReview => StoryIntelligenceResumeRoutes.Characters,
- StoryIntelligencePipelineStages.CharacterImport => StoryIntelligenceResumeRoutes.Characters,
- StoryIntelligencePipelineStages.SceneReview => StoryIntelligenceResumeRoutes.SceneReview,
- StoryIntelligencePipelineStages.SceneImport => StoryIntelligenceResumeRoutes.SceneReview,
- _ => StoryIntelligenceResumeRoutes.SceneReview
- };
+ : StoryIntelligenceResumeRoutes.SceneReview;
return new OnboardingStoryIntelligenceResumeTarget(batch.BatchID, route);
}
@@ -1092,6 +1080,22 @@ public sealed class OnboardingStoryIntelligenceService(
private static bool HasCompletedStage(StoryIntelligenceBookPipelineState state, string stage)
=> string.Equals(state.LastCompletedStage, stage, StringComparison.OrdinalIgnoreCase);
+ private static bool SceneAnalysisComplete(StoryIntelligenceBookPipelineState state)
+ => HasCompletedStage(state, StoryIntelligencePipelineStages.SceneImport)
+ || state.CurrentStage is StoryIntelligencePipelineStages.CharacterReview
+ or StoryIntelligencePipelineStages.CharacterImport
+ or StoryIntelligencePipelineStages.LocationReview
+ or StoryIntelligencePipelineStages.LocationImport
+ or StoryIntelligencePipelineStages.AssetReview
+ or StoryIntelligencePipelineStages.AssetImport
+ or StoryIntelligencePipelineStages.RelationshipReview
+ or StoryIntelligencePipelineStages.RelationshipImport
+ or StoryIntelligencePipelineStages.KnowledgeReview
+ or StoryIntelligencePipelineStages.KnowledgeImport
+ or StoryIntelligencePipelineStages.NarrativeReview
+ or StoryIntelligencePipelineStages.NarrativeImport
+ or StoryIntelligencePipelineStages.Complete;
+
private async Task<(UserOnboardingState State, ManuscriptScanPreview Preview, ManuscriptScanReviewDecision Review, OnboardingManuscriptBuildResult? Build, OnboardingWizardViewModel Wizard)?> GetContextAsync(int userId)
{
var state = await onboardingRepository.GetAsync(userId);
diff --git a/PlotLine/Services/StoryIntelligenceAssetImportService.cs b/PlotLine/Services/StoryIntelligenceAssetImportService.cs
index 041154d..f901150 100644
--- a/PlotLine/Services/StoryIntelligenceAssetImportService.cs
+++ b/PlotLine/Services/StoryIntelligenceAssetImportService.cs
@@ -82,7 +82,7 @@ public sealed class StoryIntelligenceAssetImportService(
var data = await BuildCandidateDataAsync(batch);
if (!data.HasCommittedScenes)
{
- return new StoryIntelligenceImportCommitResult { Success = false, Message = "Create scenes before importing assets." };
+ return new StoryIntelligenceImportCommitResult { Success = false, Message = "Complete Story Intelligence analysis before reviewing assets." };
}
var choices = form.Assets
@@ -311,14 +311,19 @@ public sealed class StoryIntelligenceAssetImportService(
foreach (var item in batch.Items)
{
- var commit = await runs.GetImportCommitAsync(item.RunID);
- if (commit is null || !string.Equals(commit.Status, StoryIntelligenceImportCommitStatuses.Completed, StringComparison.OrdinalIgnoreCase))
+ var run = await runs.GetRunAsync(item.RunID);
+ if (run?.Status is not (StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings))
+ {
+ continue;
+ }
+
+ var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
+ if (sceneResults.Count == 0)
{
continue;
}
hasCommittedScenes = true;
- var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
var chapterScenes = (await scenes.ListByChapterAsync(item.ChapterID)).ToList();
var importedScenes = sceneResults.Any(result => result.SceneID.HasValue)
? chapterScenes
diff --git a/PlotLine/Services/StoryIntelligenceCharacterImportService.cs b/PlotLine/Services/StoryIntelligenceCharacterImportService.cs
index c03dc77..c20d093 100644
--- a/PlotLine/Services/StoryIntelligenceCharacterImportService.cs
+++ b/PlotLine/Services/StoryIntelligenceCharacterImportService.cs
@@ -184,7 +184,7 @@ public sealed class StoryIntelligenceCharacterImportService(
var data = await BuildCandidateDataAsync(batch);
if (!data.HasCommittedScenes)
{
- return new StoryIntelligenceImportCommitResult { Success = false, Message = "Create scenes before importing characters." };
+ return new StoryIntelligenceImportCommitResult { Success = false, Message = "Complete Story Intelligence analysis before reviewing characters." };
}
var choices = form.Characters
@@ -416,14 +416,19 @@ public sealed class StoryIntelligenceCharacterImportService(
foreach (var item in batch.Items)
{
- var commit = await runs.GetImportCommitAsync(item.RunID);
- if (commit is null || !string.Equals(commit.Status, StoryIntelligenceImportCommitStatuses.Completed, StringComparison.OrdinalIgnoreCase))
+ var run = await runs.GetRunAsync(item.RunID);
+ if (run?.Status is not (StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings))
+ {
+ continue;
+ }
+
+ var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
+ if (sceneResults.Count == 0)
{
continue;
}
hasCommittedScenes = true;
- var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
var chapterScenes = (await scenes.ListByChapterAsync(item.ChapterID)).ToList();
var importedScenes = sceneResults.Any(result => result.SceneID.HasValue)
? chapterScenes
diff --git a/PlotLine/Services/StoryIntelligenceImportCommitService.cs b/PlotLine/Services/StoryIntelligenceImportCommitService.cs
index e094a8b..fdcebee 100644
--- a/PlotLine/Services/StoryIntelligenceImportCommitService.cs
+++ b/PlotLine/Services/StoryIntelligenceImportCommitService.cs
@@ -197,7 +197,7 @@ public sealed class StoryIntelligenceImportCommitService(
if (importScenes.Count == 0)
{
- blockers.Add("No scenes are ready to create for this chapter.");
+ blockers.Add("No analysed scene results are available for this chapter.");
}
var readiness = run.ChapterID.HasValue
diff --git a/PlotLine/Services/StoryIntelligenceKnowledgeImportService.cs b/PlotLine/Services/StoryIntelligenceKnowledgeImportService.cs
index b07fa08..3131ff8 100644
--- a/PlotLine/Services/StoryIntelligenceKnowledgeImportService.cs
+++ b/PlotLine/Services/StoryIntelligenceKnowledgeImportService.cs
@@ -70,7 +70,7 @@ public sealed class StoryIntelligenceKnowledgeImportService(
var data = await BuildCandidateDataAsync(batch, lookupData.KnowledgeStates);
if (!data.HasCommittedScenes)
{
- return new StoryIntelligenceImportCommitResult { Success = false, Message = "Create scenes before importing knowledge." };
+ return new StoryIntelligenceImportCommitResult { Success = false, Message = "Complete Story Intelligence analysis before reviewing knowledge." };
}
var choices = form.Knowledge
@@ -227,14 +227,19 @@ public sealed class StoryIntelligenceKnowledgeImportService(
foreach (var item in batch.Items)
{
- var commit = await runs.GetImportCommitAsync(item.RunID);
- if (commit is null || !string.Equals(commit.Status, StoryIntelligenceImportCommitStatuses.Completed, StringComparison.OrdinalIgnoreCase))
+ var run = await runs.GetRunAsync(item.RunID);
+ if (run?.Status is not (StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings))
+ {
+ continue;
+ }
+
+ var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
+ if (sceneResults.Count == 0)
{
continue;
}
hasCommittedScenes = true;
- var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
var chapterScenes = (await scenes.ListByChapterAsync(item.ChapterID)).ToList();
var importedScenes = sceneResults.Any(result => result.SceneID.HasValue)
? chapterScenes
diff --git a/PlotLine/Services/StoryIntelligenceLocationImportService.cs b/PlotLine/Services/StoryIntelligenceLocationImportService.cs
index 530cf0a..3e65293 100644
--- a/PlotLine/Services/StoryIntelligenceLocationImportService.cs
+++ b/PlotLine/Services/StoryIntelligenceLocationImportService.cs
@@ -92,7 +92,7 @@ public sealed class StoryIntelligenceLocationImportService(
var data = await BuildCandidateDataAsync(batch);
if (!data.HasCommittedScenes)
{
- return new StoryIntelligenceImportCommitResult { Success = false, Message = "Create scenes before importing locations." };
+ return new StoryIntelligenceImportCommitResult { Success = false, Message = "Complete Story Intelligence analysis before reviewing locations." };
}
var choices = form.Locations
@@ -269,14 +269,19 @@ public sealed class StoryIntelligenceLocationImportService(
foreach (var item in batch.Items)
{
- var commit = await runs.GetImportCommitAsync(item.RunID);
- if (commit is null || !string.Equals(commit.Status, StoryIntelligenceImportCommitStatuses.Completed, StringComparison.OrdinalIgnoreCase))
+ var run = await runs.GetRunAsync(item.RunID);
+ if (run?.Status is not (StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings))
+ {
+ continue;
+ }
+
+ var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
+ if (sceneResults.Count == 0)
{
continue;
}
hasCommittedScenes = true;
- var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
var chapterScenes = (await scenes.ListByChapterAsync(item.ChapterID)).ToList();
var importedScenes = sceneResults.Any(result => result.SceneID.HasValue)
? chapterScenes
diff --git a/PlotLine/Services/StoryIntelligenceNarrativeReviewService.cs b/PlotLine/Services/StoryIntelligenceNarrativeReviewService.cs
index ec5850d..15b4505 100644
--- a/PlotLine/Services/StoryIntelligenceNarrativeReviewService.cs
+++ b/PlotLine/Services/StoryIntelligenceNarrativeReviewService.cs
@@ -293,13 +293,18 @@ public sealed class StoryIntelligenceNarrativeReviewService(
foreach (var item in batch.Items)
{
- var commit = await runs.GetImportCommitAsync(item.RunID);
- if (commit is null || !string.Equals(commit.Status, StoryIntelligenceImportCommitStatuses.Completed, StringComparison.OrdinalIgnoreCase))
+ var run = await runs.GetRunAsync(item.RunID);
+ if (run?.Status is not (StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings))
{
continue;
}
var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
+ if (sceneResults.Count == 0)
+ {
+ continue;
+ }
+
var chapterScenes = (await scenes.ListByChapterAsync(item.ChapterID)).ToList();
var importedScenes = sceneResults.Any(result => result.SceneID.HasValue)
? chapterScenes
@@ -425,8 +430,14 @@ public sealed class StoryIntelligenceNarrativeReviewService(
{
foreach (var item in batch.Items)
{
- var commit = await runs.GetImportCommitAsync(item.RunID);
- if (commit is not null && string.Equals(commit.Status, StoryIntelligenceImportCommitStatuses.Completed, StringComparison.OrdinalIgnoreCase))
+ var run = await runs.GetRunAsync(item.RunID);
+ if (run?.Status is not (StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings))
+ {
+ continue;
+ }
+
+ var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
+ if (sceneResults.Count > 0)
{
return true;
}
diff --git a/PlotLine/Services/StoryIntelligenceRelationshipImportService.cs b/PlotLine/Services/StoryIntelligenceRelationshipImportService.cs
index 49b946c..d974234 100644
--- a/PlotLine/Services/StoryIntelligenceRelationshipImportService.cs
+++ b/PlotLine/Services/StoryIntelligenceRelationshipImportService.cs
@@ -65,7 +65,7 @@ public sealed class StoryIntelligenceRelationshipImportService(
var data = await BuildCandidateDataAsync(batch);
if (!data.HasCommittedScenes)
{
- return new StoryIntelligenceImportCommitResult { Success = false, Message = "Create scenes before importing relationships." };
+ return new StoryIntelligenceImportCommitResult { Success = false, Message = "Complete Story Intelligence analysis before reviewing relationships." };
}
var choices = form.Relationships
@@ -261,14 +261,19 @@ public sealed class StoryIntelligenceRelationshipImportService(
foreach (var item in batch.Items)
{
- var commit = await runs.GetImportCommitAsync(item.RunID);
- if (commit is null || !string.Equals(commit.Status, StoryIntelligenceImportCommitStatuses.Completed, StringComparison.OrdinalIgnoreCase))
+ var run = await runs.GetRunAsync(item.RunID);
+ if (run?.Status is not (StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings))
+ {
+ continue;
+ }
+
+ var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
+ if (sceneResults.Count == 0)
{
continue;
}
hasCommittedScenes = true;
- var sceneResults = await runs.ListSceneResultsAsync(item.RunID);
var chapterScenes = (await scenes.ListByChapterAsync(item.ChapterID)).ToList();
var importedScenes = sceneResults.Any(result => result.SceneID.HasValue)
? chapterScenes
diff --git a/PlotLine/Services/StoryIntelligenceService.cs b/PlotLine/Services/StoryIntelligenceService.cs
index 9d46b55..2d2d707 100644
--- a/PlotLine/Services/StoryIntelligenceService.cs
+++ b/PlotLine/Services/StoryIntelligenceService.cs
@@ -182,7 +182,7 @@ public sealed class StoryIntelligenceService(
ActiveImportProgressLabel = $"Chapter {currentChapter:N0} of {activeBatch.Items.Count:N0}",
Title = "Story Intelligence is reading",
Description = "PlotDirector is analysing your manuscript in the background.",
- ButtonText = "View progress",
+ ButtonText = "Open book",
PipelineBooks = pipelineBooks
};
}
@@ -303,7 +303,7 @@ public sealed class StoryIntelligenceService(
StoryIntelligencePipelineStages.AssetReview => "Open the Review Centre and choose which saved category to handle.",
StoryIntelligencePipelineStages.LocationReview => "Open the Review Centre and choose which saved category to handle.",
StoryIntelligencePipelineStages.CharacterReview => "Resolve character identities before reviewing dependent story intelligence.",
- StoryIntelligencePipelineStages.SceneReview => "Review prepared scenes before creating them.",
+ StoryIntelligencePipelineStages.SceneReview => "Story Intelligence analysis has not finished for this book.",
_ when StoryIntelligenceReadyForKnowledge(state) => "Open the Review Centre and choose which saved category to handle.",
_ when StoryIntelligenceReadyForRelationships(state) => "Open the Review Centre and choose which saved category to handle.",
_ when StoryIntelligenceReadyForAssets(state) => "Open the Review Centre and choose which saved category to handle.",
diff --git a/PlotLine/ViewModels/OnboardingViewModels.cs b/PlotLine/ViewModels/OnboardingViewModels.cs
index 686cefa..6106389 100644
--- a/PlotLine/ViewModels/OnboardingViewModels.cs
+++ b/PlotLine/ViewModels/OnboardingViewModels.cs
@@ -167,6 +167,7 @@ public sealed class StoryIntelligenceProgressViewModel
public int FailedChapterCount => Chapters.Count(chapter => chapter.IsFailed);
public int TotalDetectedScenes => Chapters.Sum(chapter => chapter.Scenes.Count > 0 ? chapter.Scenes.Count : chapter.TotalDetectedScenes ?? 0);
public int TotalCompletedScenes => Chapters.Sum(chapter => chapter.CompletedScenes ?? 0);
+ public int TotalAnalysedScenes => Math.Max(TotalCompletedScenes, PipelineDashboard.ScenesImported);
public int TotalFailedScenes => Chapters.Sum(chapter => chapter.FailedScenes ?? 0);
public int TotalAnalysedWords => Chapters.Sum(chapter => chapter.SourceWordCount ?? 0);
public decimal? TotalEstimatedCostUSD => Chapters.Any(chapter => chapter.EstimatedCostUSD.HasValue) ? Chapters.Sum(chapter => chapter.EstimatedCostUSD ?? 0m) : null;
@@ -174,6 +175,8 @@ public sealed class StoryIntelligenceProgressViewModel
public bool HasActiveRuns => Chapters.Any(chapter => chapter.IsActive);
public bool AllCommitted => Chapters.Count > 0 && Chapters.All(chapter => chapter.HasCompletedCommit);
public bool HasReadyScenesToCreate => Chapters.Any(chapter => chapter.CanCommit);
+ public bool SceneAnalysisComplete => PipelineDashboard.SceneAnalysisComplete
+ || (Chapters.Count > 0 && !HasActiveRuns && Chapters.All(chapter => chapter.IsRunComplete));
public bool HasCharactersToReview => CharacterReview.Candidates.Count > 0;
public bool HasLocationsToReview => LocationReview.Candidates.Count > 0;
public bool HasAssetsToReview => AssetReview.Candidates.Count > 0;
@@ -201,6 +204,7 @@ public sealed class StoryIntelligencePipelineDashboardViewModel
{
public int ChaptersAnalysed { get; init; }
public int ScenesImported { get; init; }
+ public bool SceneAnalysisComplete { get; init; }
public int CharactersIdentified { get; init; }
public int CharactersCreatedOrLinked { get; init; }
public bool CharacterStageComplete { get; init; }
diff --git a/PlotLine/Views/Books/Details.cshtml b/PlotLine/Views/Books/Details.cshtml
index 53d0b05..aefbd00 100644
--- a/PlotLine/Views/Books/Details.cshtml
+++ b/PlotLine/Views/Books/Details.cshtml
@@ -230,8 +230,8 @@
StoryIntelligencePipelineStages.RelationshipReview => "Open the Review Centre and choose which saved category to handle.",
StoryIntelligencePipelineStages.AssetReview => "Open the Review Centre and choose which saved category to handle.",
StoryIntelligencePipelineStages.LocationReview => "Open the Review Centre and choose which saved category to handle.",
- StoryIntelligencePipelineStages.CharacterReview => "Scene creation is complete. Resolve character identities before reviewing dependent story intelligence.",
- StoryIntelligencePipelineStages.SceneReview => "Review prepared scenes before creating them in PlotDirector.",
+ StoryIntelligencePipelineStages.CharacterReview => "Analysis is complete. Resolve character identities before reviewing dependent story intelligence.",
+ StoryIntelligencePipelineStages.SceneReview => "Story Intelligence analysis has not finished for this book.",
_ when StoryIntelligenceReadyForNarrative(pipeline) => "Open the Review Centre and choose which saved category to handle.",
_ when StoryIntelligenceReadyForRelationships(pipeline) => "Open the Review Centre and choose which saved category to handle.",
_ when StoryIntelligenceReadyForAssets(pipeline) => "Open the Review Centre and choose which saved category to handle.",
diff --git a/PlotLine/Views/Onboarding/BuildComplete.cshtml b/PlotLine/Views/Onboarding/BuildComplete.cshtml
index 10e755a..a99f655 100644
--- a/PlotLine/Views/Onboarding/BuildComplete.cshtml
+++ b/PlotLine/Views/Onboarding/BuildComplete.cshtml
@@ -22,10 +22,10 @@
{
@:This manuscript structure has already been created in PlotDirector, so nothing was duplicated. Review characters to complete the core import.
}
- else
- {
- @:@Model.ScenesCreated.ToString("N0") scenes detected across @Model.ChaptersCreated.ToString("N0") chapters. Keep this browser workflow open to review characters.
- }
+ else
+ {
+ @:@Model.ScenesCreated.ToString("N0") scenes imported across @Model.ChaptersCreated.ToString("N0") chapters. Keep this browser workflow open to review characters.
+ }
@if (!string.IsNullOrWhiteSpace(Model.MarkerWarning))
{
diff --git a/PlotLine/Views/Onboarding/StoryIntelligence.cshtml b/PlotLine/Views/Onboarding/StoryIntelligence.cshtml
index 349cd48..d5cdd90 100644
--- a/PlotLine/Views/Onboarding/StoryIntelligence.cshtml
+++ b/PlotLine/Views/Onboarding/StoryIntelligence.cshtml
@@ -27,7 +27,7 @@
Analysis has already started
You can return to progress or open the Review Centre when analysis is ready.
-
View progress
+
Return to book
@@ -72,7 +72,7 @@
1Analyse chapters
-
2Create scene intelligence
+
2Analyse canonical scenes
3Resolve characters
4Open Review Centre
diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceContext.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceContext.cshtml
index 4cafae1..652a99a 100644
--- a/PlotLine/Views/Onboarding/StoryIntelligenceContext.cshtml
+++ b/PlotLine/Views/Onboarding/StoryIntelligenceContext.cshtml
@@ -23,7 +23,7 @@
Analysis has already started
You can return to progress or open the Review Centre when analysis is ready.
diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml
deleted file mode 100644
index 2eda890..0000000
--- a/PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml
+++ /dev/null
@@ -1,326 +0,0 @@
-@model StoryIntelligenceProgressViewModel
-@using System.Security.Claims
-@inject IConfiguration Configuration
-@{
- ViewData["Title"] = "Create scene intelligence";
- var adminEmails = Configuration.GetSection("Admin:AllowedEmails").Get
() ?? [];
- var userEmail = User.FindFirstValue(ClaimTypes.Email);
- var isAdmin = !string.IsNullOrWhiteSpace(userEmail)
- && adminEmails.Any(email => string.Equals(email, userEmail, StringComparison.OrdinalIgnoreCase));
- var chaptersNeedingReview = Model.Chapters.Count(NeedsReview);
- var warningsCount = Model.Chapters.Sum(chapter => NeedsReviewCount(chapter));
-}
-
-
-
-
-
-
Phase 2
-
Create Scene Intelligence
-
Create Scene Intelligence
-
PlotDirector has finished reading your manuscript. Create the stored scene intelligence before opening the Review Centre.
-
-
- @if (TempData["OnboardingStoryIntelligenceError"] is string error)
- {
-
@error
- }
- @if (TempData["OnboardingStoryIntelligenceMessage"] is string message)
- {
-
@message
- }
-
-
- Scene intelligence is created first. Character resolution is reviewed separately; after that, locations, assets, relationships, knowledge, and Plot Lines can be reviewed independently.
-
-
-
-
- Story Intelligence complete
- @Model.CompletedChapterCount.ToString("N0") chapter@(Model.CompletedChapterCount == 1 ? string.Empty : "s")
-
-
- Scenes detected
- @Model.TotalDetectedScenes.ToString("N0")
-
-
- Words analysed
- @Model.TotalAnalysedWords.ToString("N0")
-
-
- Elapsed time
- @FormatDuration(Model.TotalDurationMs)
-
-
- Estimated AI cost
- @FormatCost(Model.TotalEstimatedCostUSD)
-
-
- Needs review
- @warningsCount.ToString("N0")
-
-
-
-
- @foreach (var chapter in Model.Chapters.OrderBy(chapter => chapter.ChapterNumber))
- {
-
-
-
-
Chapter @chapter.ChapterNumber
-
@chapter.ChapterTitle
-
@DetectedCount(chapter).ToString("N0") scene@(DetectedCount(chapter) == 1 ? string.Empty : "s") detected / @ReadyCount(chapter).ToString("N0") ready to create
-
- @FriendlyStatus(chapter)
-
-
-
-
- Detected scenes
- @DetectedCount(chapter).ToString("N0")
-
-
- Ready
- @ReadyCount(chapter).ToString("N0")
-
-
- Needs review
- @NeedsReviewCount(chapter).ToString("N0")
-
-
- Rejected
- @RejectedCount(chapter).ToString("N0")
-
-
-
- @if (!string.IsNullOrWhiteSpace(chapter.CommitMessage))
- {
- @chapter.CommitMessage
- }
-
- @if (chapter.Warnings.Count > 0)
- {
-
-
Notes
-
- @foreach (var warning in chapter.Warnings)
- {
- - @AuthorMessage(warning)
- }
-
-
- }
-
- @if (chapter.Blockers.Count > 0)
- {
-
-
Before scenes can be created
-
- @foreach (var blocker in chapter.Blockers)
- {
- - @AuthorMessage(blocker)
- }
-
-
- }
-
- @if (!string.IsNullOrWhiteSpace(chapter.ErrorMessage))
- {
- @AuthorMessage(chapter.ErrorMessage)
- }
-
-
- @if (chapter.Scenes.Count == 0)
- {
-
No readable scene previews are available for this chapter.
- }
- else
- {
- @foreach (var scene in chapter.Scenes)
- {
-
-
- Scene @scene.SceneNumber.ToString("N0")
- @if (scene.HasWarnings)
- {
- @scene.AnalysisStatus
- }
-
- @scene.Summary
-
- - POV
- @Display(scene.Pov)
- - Setting
- @Display(scene.Setting)
- - Confidence
- @FormatConfidence(scene.Confidence)
-
-
- }
- }
-
-
- @if (isAdmin)
- {
-
- Admin diagnostics
-
- - Run ID
- @chapter.RunID
- - Status
- @chapter.Status
- - Stage
- @(chapter.CurrentStage ?? "-")
- - Failure stage
- @(chapter.FailureStage ?? "-")
- - Tokens
- @(chapter.TotalTokens?.ToString("N0") ?? "-")
-
- Open diagnostics
-
- }
-
- }
-
-
-
- Scene creation uses PlotDirector import safeguards. Duplicate imports are blocked, and failures roll back safely.
-
-
-
-
-
-
-@section Scripts {
-
-}
-
-@functions {
- private static bool NeedsReview(StoryIntelligenceOnboardingChapterViewModel chapter)
- => chapter.IsFailed
- || chapter.Blockers.Count > 0
- || NeedsReviewCount(chapter) > 0
- || (!chapter.CanCommit && !chapter.HasCompletedCommit && chapter.IsRunComplete);
-
- private static int DetectedCount(StoryIntelligenceOnboardingChapterViewModel chapter)
- => chapter.Scenes.Count > 0 ? chapter.Scenes.Count : chapter.TotalDetectedScenes ?? 0;
-
- private static int ReadyCount(StoryIntelligenceOnboardingChapterViewModel chapter)
- => chapter.HasCompletedCommit ? chapter.ScenesCreated : chapter.Scenes.Count(scene => !scene.HasWarnings);
-
- private static int RejectedCount(StoryIntelligenceOnboardingChapterViewModel chapter) => 0;
-
- private static int NeedsReviewCount(StoryIntelligenceOnboardingChapterViewModel chapter)
- => chapter.Scenes.Count > 0
- ? chapter.Scenes.Count(scene => scene.HasWarnings)
- : Math.Max(0, DetectedCount(chapter) - ReadyCount(chapter) - RejectedCount(chapter));
-
- private static string FriendlyStatus(StoryIntelligenceOnboardingChapterViewModel chapter)
- {
- if (chapter.HasCompletedCommit)
- {
- return "Scenes created";
- }
-
- if (chapter.CanCommit)
- {
- return "Ready";
- }
-
- return chapter.Status switch
- {
- StoryIntelligenceRunStatuses.Pending => "Waiting",
- StoryIntelligenceRunStatuses.Running => "Reading",
- StoryIntelligenceRunStatuses.Completed => "Cannot import",
- StoryIntelligenceRunStatuses.CompletedWithWarnings => "Cannot import",
- StoryIntelligenceRunStatuses.Failed => "Needs review",
- StoryIntelligenceRunStatuses.Cancelled => "Cancelled",
- _ => "Needs review"
- };
- }
-
- private static string StatusClass(StoryIntelligenceOnboardingChapterViewModel chapter)
- {
- if (chapter.HasCompletedCommit)
- {
- return "story-review-status story-review-status--created";
- }
-
- if (chapter.CanCommit)
- {
- return "story-review-status story-review-status--ready";
- }
-
- return chapter.IsActive
- ? "story-review-status story-review-status--active"
- : "story-review-status story-review-status--warning";
- }
-
- private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "Not detected" : value;
-
- private static string AuthorMessage(string? value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return "This chapter needs review before scenes can be created.";
- }
-
- var message = value.Trim();
- if (message.Contains("JSON", StringComparison.OrdinalIgnoreCase)
- || message.Contains("parsed", StringComparison.OrdinalIgnoreCase)
- || message.Contains("parser", StringComparison.OrdinalIgnoreCase)
- || message.Contains("deserial", StringComparison.OrdinalIgnoreCase))
- {
- return "One or more scene suggestions could not be prepared automatically and need review.";
- }
-
- if (message.Contains("existing authored scenes", StringComparison.OrdinalIgnoreCase)
- || message.Contains("already contains scenes", StringComparison.OrdinalIgnoreCase))
- {
- return "This chapter already contains scenes. Choose an empty chapter before creating scenes from this analysis.";
- }
-
- return message;
- }
-
- private static string FormatConfidence(decimal? confidence)
- {
- if (!confidence.HasValue)
- {
- return "Not available";
- }
-
- var value = Math.Clamp(confidence.Value, 0m, 1m);
- return value >= 0.75m
- ? "High"
- : value >= 0.45m
- ? "Medium"
- : "Low";
- }
-
- private static string FormatCost(decimal? cost)
- => cost.HasValue ? $"About ${cost.Value:0.00}" : "Not available";
-
- private static string FormatDuration(long milliseconds)
- {
- var elapsed = TimeSpan.FromMilliseconds(Math.Max(0, milliseconds));
- if (elapsed.TotalHours >= 1)
- {
- return $"{Math.Ceiling(elapsed.TotalHours):N0} hr";
- }
-
- return elapsed.TotalMinutes >= 1
- ? $"{Math.Max(1, (int)Math.Round(elapsed.TotalMinutes)):N0} min"
- : $"{Math.Max(0, (int)Math.Round(elapsed.TotalSeconds)):N0} sec";
- }
-}
diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml
index 1d8c2e6..7787e0a 100644
--- a/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml
+++ b/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml
@@ -10,7 +10,7 @@
Story Intelligence
Review Centre
-
Review only the story intelligence that needs an author decision. Everything here is saved and can be picked up later.
+
Your manuscript analysis is complete. Review only the story intelligence that needs an author decision; everything here is saved and can be picked up later.
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
@@ -28,9 +28,9 @@
@ReviewModule(
"Characters",
Model.CharacterNeedsAttentionCount,
- Model.PipelineDashboard.CharacterStageComplete ? "Resolved" : "Needs attention",
+ Model.CharacterGateSatisfied ? "Resolved" : "Needs attention",
"Resolve identity questions before reviewing dependent story intelligence.",
- "Review characters",
+ "Resolve Characters",
"StoryIntelligenceCharacters",
false)
@@ -84,8 +84,16 @@
Scene Metrics
Available from the imported scene analysis. Metrics are auto-applied where configured and do not require a mandatory review step.
- Available
- 0 need attention
+ @if (Model.CharacterGateSatisfied)
+ {
+ Available
+ @Model.TotalAnalysedScenes.ToString("N0") scenes
+ }
+ else
+ {
+ Resolve Characters first
+ Locked until Characters are resolved
+ }
@if (Model.AnalysisIssueCount > 0)
@@ -96,7 +104,7 @@
Some chapters or scenes reported analysis issues. Canonical chapters and scenes remain intact.
Needs attention
- View issues
+ Return to book
@Model.AnalysisIssueCount.ToString("N0") need attention
}
@@ -126,9 +134,9 @@
: count > 0
? "story-review-status story-review-status--warning"
: "story-review-status story-review-status--ready";
- var statusText = blocked ? "Blocked by character resolution" : status;
+ var statusText = blocked ? "Resolve Characters first" : status;
var countText = blocked
- ? "Resolve characters first"
+ ? "Locked until Characters are resolved"
: count == 1
? "1 needs attention"
: $"{count:N0} need attention";