diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 1e8d8bf..355b395 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -223,7 +223,13 @@ var tests = new (string Name, Action Test)[] ("Word manuscript cleanup preserves content controls content", WordManuscriptCleanupPreservesContentControlContent), ("Word manuscript cleanup UI separates unlink from clean unlink", WordManuscriptCleanupUiSeparatesUnlinkFromCleanUnlink), ("Word manuscript unlink keeps Story Intelligence data untouched", WordManuscriptUnlinkKeepsStoryIntelligenceDataUntouched), - ("Word manuscript cleanup remains relinkable and idempotent", WordManuscriptCleanupRemainsRelinkableAndIdempotent) + ("Word manuscript cleanup remains relinkable and idempotent", WordManuscriptCleanupRemainsRelinkableAndIdempotent), + ("Story Intelligence Review Centre route replaces linear review resume", StoryIntelligenceReviewCentreRouteReplacesLinearReviewResume), + ("Story Intelligence Review Centre counts only needs-attention categories", StoryIntelligenceReviewCentreCountsOnlyNeedsAttentionCategories), + ("Story Intelligence review modules are independent after character gate", StoryIntelligenceReviewModulesAreIndependentAfterCharacterGate), + ("Story Intelligence review posts do not use success interstitials", StoryIntelligenceReviewPostsDoNotUseSuccessInterstitials), + ("Story Intelligence review forms share busy submit guard", StoryIntelligenceReviewFormsShareBusySubmitGuard), + ("Story Intelligence Review Centre adds no AI calls", StoryIntelligenceReviewCentreAddsNoAiCalls) }; foreach (var test in tests) @@ -232,6 +238,114 @@ foreach (var test in tests) Console.WriteLine($"PASS {test.Name}"); } +static void StoryIntelligenceReviewCentreRouteReplacesLinearReviewResume() +{ + var root = PlotLineRoot(); + var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs")); + var service = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs")); + var view = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceReviewCentre.cshtml")); + + 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."); +} + +static void StoryIntelligenceReviewCentreCountsOnlyNeedsAttentionCategories() +{ + var root = PlotLineRoot(); + var viewModel = File.ReadAllText(Path.Combine(root, "ViewModels/OnboardingViewModels.cs")); + var centre = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceReviewCentre.cshtml")); + var summary = File.ReadAllText(Path.Combine(root, "Views/Onboarding/_StoryIntelligencePipelineSummary.cshtml")); + + 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(!summary.Contains("Locations identified", StringComparison.Ordinal), "Summary should not present total identified rows as review workload."); +} + +static void StoryIntelligenceReviewModulesAreIndependentAfterCharacterGate() +{ + var root = PlotLineRoot(); + var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs")); + var locationsAction = ExtractBetween(controller, "public async Task StoryIntelligenceLocations", "[HttpGet(\"story-intelligence/assets\")]"); + var assetsAction = ExtractBetween(controller, "public async Task StoryIntelligenceAssets", "[HttpGet(\"story-intelligence/relationships\")]"); + var relationshipsAction = ExtractBetween(controller, "public async Task StoryIntelligenceRelationships", "[HttpPost(\"story-intelligence/cancel\")]"); + var knowledgeAction = ExtractBetween(controller, "public async Task StoryIntelligenceKnowledge", "[HttpPost(\"story-intelligence/knowledge\")]"); + var narrativeAction = ExtractBetween(controller, "public async Task StoryIntelligenceNarrative", "[HttpPost(\"story-intelligence/narrative\")]"); + + Assert(!assetsAction.Contains("LocationStageComplete", StringComparison.Ordinal), "Assets should not require completed location review."); + 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."); +} + +static void StoryIntelligenceReviewPostsDoNotUseSuccessInterstitials() +{ + var root = PlotLineRoot(); + var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs")); + var postBlock = ExtractBetween(controller, "[HttpPost(\"story-intelligence/characters\")]", "[HttpGet(\"story-intelligence/characters/complete\")]") + + ExtractBetween(controller, "[HttpPost(\"story-intelligence/locations\")]", "[HttpGet(\"story-intelligence/locations/complete\")]") + + ExtractBetween(controller, "[HttpPost(\"story-intelligence/assets\")]", "[HttpGet(\"story-intelligence/assets/complete\")]") + + ExtractBetween(controller, "[HttpPost(\"story-intelligence/relationships\")]", "[HttpGet(\"story-intelligence/relationships/complete\")]") + + ExtractBetween(controller, "[HttpPost(\"story-intelligence/knowledge\")]", "[HttpGet(\"story-intelligence/knowledge/complete\")]") + + ExtractBetween(controller, "[HttpPost(\"story-intelligence/narrative\")]", "[HttpGet(\"story-intelligence/narrative/complete\")]"); + + Assert(postBlock.Contains("StoryIntelligenceReviewCentre", StringComparison.Ordinal), "Review posts should return to the centre."); + Assert(!postBlock.Contains("StoryIntelligenceCharacterComplete", StringComparison.Ordinal), "Character post should not target success interstitial."); + Assert(!postBlock.Contains("StoryIntelligenceLocationComplete", StringComparison.Ordinal), "Location post should not target success interstitial."); + Assert(!postBlock.Contains("StoryIntelligenceAssetComplete", StringComparison.Ordinal), "Asset post should not target success interstitial."); + Assert(!postBlock.Contains("StoryIntelligenceRelationshipComplete", StringComparison.Ordinal), "Relationship post should not target success interstitial."); + Assert(!postBlock.Contains("StoryIntelligenceKnowledgeComplete", StringComparison.Ordinal), "Knowledge post should not target success interstitial."); + Assert(!postBlock.Contains("StoryIntelligenceNarrativeComplete", StringComparison.Ordinal), "Narrative post should not target success interstitial."); +} + +static void StoryIntelligenceReviewFormsShareBusySubmitGuard() +{ + var root = PlotLineRoot(); + var script = File.ReadAllText(Path.Combine(root, "wwwroot/js/story-review-actions.js")); + var reviewViews = new[] + { + "StoryIntelligenceCharacters.cshtml", + "StoryIntelligenceLocations.cshtml", + "StoryIntelligenceAssets.cshtml", + "StoryIntelligenceRelationships.cshtml", + "StoryIntelligenceKnowledge.cshtml", + "StoryIntelligenceNarrative.cshtml" + }; + + Assert(script.Contains("data-story-review-form", StringComparison.Ordinal), "Shared busy helper should bind review forms."); + Assert(script.Contains("form.dataset.submitting === \"true\"", StringComparison.Ordinal), "Shared busy helper should prevent double submit."); + Assert(script.Contains("aria-busy", StringComparison.Ordinal), "Busy state should be exposed accessibly."); + foreach (var viewName in reviewViews) + { + var view = File.ReadAllText(Path.Combine(root, "Views/Onboarding", viewName)); + Assert(view.Contains("data-story-review-form", StringComparison.Ordinal), $"{viewName} should opt into the shared busy helper."); + Assert(view.Contains("story-review-actions.js", StringComparison.Ordinal), $"{viewName} should load the shared busy helper."); + } +} + +static void StoryIntelligenceReviewCentreAddsNoAiCalls() +{ + 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("StartAsync", StringComparison.Ordinal) && !centreAction.Contains("StartBookAsync", StringComparison.Ordinal), "Review Centre should not queue AI work."); + Assert(!centreAction.Contains("EnsureCanQueueAsync", StringComparison.Ordinal), "Review Centre should not perform paid AI queue checks."); + Assert(!centreView.Contains("OpenAI", StringComparison.Ordinal) && !centreView.Contains("prompt", StringComparison.OrdinalIgnoreCase), "Review Centre view should not introduce prompt work."); +} + +static string PlotLineRoot() + => Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine"); + +static string ExtractBetween(string value, string start, string end) + => Between(value, start, end); + static void AdminStoryIntelligenceResetReturnsToDiagnosticsWithMessages() { var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine"); diff --git a/PlotLine/Controllers/OnboardingController.cs b/PlotLine/Controllers/OnboardingController.cs index 2c9ac05..4a1502b 100644 --- a/PlotLine/Controllers/OnboardingController.cs +++ b/PlotLine/Controllers/OnboardingController.cs @@ -115,7 +115,8 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return target.Route switch { - StoryIntelligenceResumeRoutes.Complete => RedirectToAction(nameof(StoryIntelligenceComplete), new { batchId = target.BatchID }), + 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 }), @@ -211,6 +212,30 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard : View(model); } + [HttpGet("story-intelligence/review-centre")] + public async Task StoryIntelligenceReviewCentre(Guid batchId) + { + var model = await storyIntelligence.GetProgressAsync(batchId); + if (model is null) + { + return NotFound(); + } + + if (model.HasActiveRuns) + { + return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId }); + } + + if (!model.AllCommitted) + { + return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId }); + } + + return !model.PipelineDashboard.CharacterStageComplete + ? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId }) + : View(model); + } + [HttpGet("story-intelligence/characters")] public async Task StoryIntelligenceCharacters(Guid batchId) { @@ -225,8 +250,13 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId }); } - return !model.AllCommitted - ? RedirectToAction(nameof(StoryIntelligenceReview), new { batchId }) + if (!model.AllCommitted) + { + return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId }); + } + + return model.PipelineDashboard.CharacterStageComplete && model.CharacterReview.Candidates.Count == 0 + ? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId }) : View(model); } @@ -278,9 +308,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId }); } - return !model.PipelineDashboard.LocationStageComplete - ? RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId }) - : View(model); + return View(model); } [HttpGet("story-intelligence/relationships")] @@ -307,14 +335,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId }); } - if (!model.PipelineDashboard.LocationStageComplete) - { - return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId }); - } - - return !model.PipelineDashboard.AssetStageComplete - ? RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId }) - : View(model); + return View(model); } [HttpPost("story-intelligence/cancel")] @@ -344,8 +365,9 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; if (result.Success && progress.AllCommitted) { - TempData["WriterWorkspaceMessage"] = $"{progress.CommittedChapterCount} chapter(s) and {progress.Chapters.Sum(chapter => chapter.ScenesCreated)} scene(s) created from your manuscript."; - return RedirectToAction("Index", "Writer", new { projectId = progress.ProjectID }); + return RedirectToAction(progress.PipelineDashboard.CharacterStageComplete || progress.CharacterReview.Candidates.Count == 0 + ? nameof(StoryIntelligenceReviewCentre) + : nameof(StoryIntelligenceCharacters), new { batchId }); } return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId }); @@ -363,7 +385,9 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; return result.Success && progress.AllCommitted - ? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId }) + ? RedirectToAction(progress.PipelineDashboard.CharacterStageComplete || progress.CharacterReview.Candidates.Count == 0 + ? nameof(StoryIntelligenceReviewCentre) + : nameof(StoryIntelligenceCharacters), new { batchId }) : RedirectToAction(nameof(StoryIntelligenceReview), new { batchId }); } @@ -379,7 +403,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; return result.Success - ? RedirectToAction(nameof(StoryIntelligenceCharacterComplete), new { batchId = form.BatchID }) + ? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }) : RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId = form.BatchID }); } @@ -402,7 +426,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; return result.Success - ? RedirectToAction(nameof(StoryIntelligenceLocationComplete), new { batchId = form.BatchID }) + ? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }) : RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = form.BatchID }); } @@ -425,7 +449,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; return result.Success - ? RedirectToAction(nameof(StoryIntelligenceAssetComplete), new { batchId = form.BatchID }) + ? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }) : RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId = form.BatchID }); } @@ -448,7 +472,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; return result.Success - ? RedirectToAction(nameof(StoryIntelligenceRelationshipComplete), new { batchId = form.BatchID }) + ? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }) : RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId = form.BatchID }); } @@ -483,19 +507,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId }); } - if (!model.PipelineDashboard.LocationStageComplete) - { - return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId }); - } - - if (!model.PipelineDashboard.AssetStageComplete) - { - return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId }); - } - - return !model.PipelineDashboard.RelationshipStageComplete - ? RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId }) - : View(model); + return View(model); } [HttpPost("story-intelligence/knowledge")] @@ -510,7 +522,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; return result.Success - ? RedirectToAction(nameof(StoryIntelligenceKnowledgeComplete), new { batchId = form.BatchID }) + ? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }) : RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = form.BatchID }); } @@ -545,24 +557,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId }); } - if (!model.PipelineDashboard.LocationStageComplete) - { - return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId }); - } - - if (!model.PipelineDashboard.AssetStageComplete) - { - return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId }); - } - - if (!model.PipelineDashboard.RelationshipStageComplete) - { - return RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId }); - } - - return !model.PipelineDashboard.KnowledgeStageComplete - ? RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId }) - : View(model); + return View(model); } [HttpPost("story-intelligence/narrative")] @@ -577,7 +572,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; return result.Success - ? RedirectToAction(nameof(StoryIntelligenceNarrativeComplete), new { batchId = form.BatchID }) + ? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }) : RedirectToAction(nameof(StoryIntelligenceNarrative), new { batchId = form.BatchID }); } @@ -602,31 +597,6 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId }); } - if (!progress.PipelineDashboard.LocationStageComplete) - { - return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId }); - } - - if (!progress.PipelineDashboard.AssetStageComplete) - { - return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId }); - } - - if (!progress.PipelineDashboard.RelationshipStageComplete) - { - return RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId }); - } - - if (!progress.PipelineDashboard.KnowledgeStageComplete) - { - return RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId }); - } - - if (!progress.PipelineDashboard.NarrativeStageComplete) - { - return RedirectToAction(nameof(StoryIntelligenceNarrative), new { batchId }); - } - var model = await storyIntelligence.GetCompletionAsync(batchId); return model is null ? NotFound() : View(model); } diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index 5fb1643..91382b7 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -1385,10 +1385,10 @@ public sealed class BookService( HasCanonicalSceneSources = hasCanonicalSceneSources, CanStart = false, StatusLabel = pipeline.IsComplete ? "Complete" : pipeline.Status, - ActionLabel = pipeline.IsComplete ? "View Story Intelligence Summary" : "Continue Story Intelligence", + ActionLabel = "Open Review Centre", Description = pipeline.IsComplete - ? "All currently available Story Intelligence stages are complete." - : "Story Intelligence has saved progress for this book." + ? "Story Intelligence review is saved for this book." + : "Story Intelligence has saved review categories for this book." }; } diff --git a/PlotLine/Services/OnboardingStoryIntelligenceService.cs b/PlotLine/Services/OnboardingStoryIntelligenceService.cs index ea44c35..c7da453 100644 --- a/PlotLine/Services/OnboardingStoryIntelligenceService.cs +++ b/PlotLine/Services/OnboardingStoryIntelligenceService.cs @@ -1069,19 +1069,9 @@ public sealed class OnboardingStoryIntelligenceService( await batchStore.SaveAsync(batch); - var route = narrativeStageComplete - ? StoryIntelligenceResumeRoutes.Complete - : knowledgeStageComplete - ? StoryIntelligenceResumeRoutes.Narrative - : relationshipStageComplete - ? StoryIntelligenceResumeRoutes.Knowledge - : assetStageComplete - ? StoryIntelligenceResumeRoutes.Relationships - : locationStageComplete - ? StoryIntelligenceResumeRoutes.Assets - : characterStageComplete - ? StoryIntelligenceResumeRoutes.Locations - : state.CurrentStage switch + var route = characterStageComplete + ? StoryIntelligenceResumeRoutes.ReviewCentre + : state.CurrentStage switch { StoryIntelligencePipelineStages.RelationshipReview => StoryIntelligenceResumeRoutes.Relationships, StoryIntelligencePipelineStages.RelationshipImport => StoryIntelligenceResumeRoutes.Relationships, @@ -1540,6 +1530,7 @@ public sealed class StoryIntelligenceNarrativeImportBatchResult public static class StoryIntelligenceResumeRoutes { public const string SceneReview = "SceneReview"; + public const string ReviewCentre = "ReviewCentre"; public const string Characters = "Characters"; public const string Locations = "Locations"; public const string Assets = "Assets"; diff --git a/PlotLine/Services/StoryIntelligenceService.cs b/PlotLine/Services/StoryIntelligenceService.cs index f22543a..56e79e1 100644 --- a/PlotLine/Services/StoryIntelligenceService.cs +++ b/PlotLine/Services/StoryIntelligenceService.cs @@ -194,11 +194,11 @@ public sealed class StoryIntelligenceService( return new StoryIntelligenceDashboardViewModel { ShouldShow = true, - Title = nextPipeline.IsComplete ? "Story Intelligence Complete" : "Continue Story Intelligence", + Title = "Story Intelligence Review Centre", Description = nextPipeline.IsComplete - ? $"{nextPipeline.BookTitle} has completed the currently available Story Intelligence stages." - : $"{nextPipeline.BookTitle} is waiting at the next Story Intelligence review stage.", - ButtonText = nextPipeline.IsComplete ? "View summary" : "Continue Story Intelligence", + ? $"{nextPipeline.BookTitle} has saved Story Intelligence review." + : $"{nextPipeline.BookTitle} has saved Story Intelligence review categories.", + ButtonText = "Open Review Centre", PipelineBooks = pipelineBooks }; } @@ -264,21 +264,21 @@ public sealed class StoryIntelligenceService( { if (state.IsComplete) { - return "Story Intelligence Complete"; + return "Story Intelligence Review Saved"; } return state.CurrentReviewStage switch { - StoryIntelligencePipelineStages.KnowledgeReview => "Waiting for Knowledge Review", - StoryIntelligencePipelineStages.RelationshipReview => "Waiting for Relationship Review", - StoryIntelligencePipelineStages.AssetReview => "Waiting for Asset Review", - StoryIntelligencePipelineStages.LocationReview => "Waiting for Location Review", - StoryIntelligencePipelineStages.CharacterReview => "Waiting for Character Review", + StoryIntelligencePipelineStages.KnowledgeReview => "Review Centre", + StoryIntelligencePipelineStages.RelationshipReview => "Review Centre", + StoryIntelligencePipelineStages.AssetReview => "Review Centre", + StoryIntelligencePipelineStages.LocationReview => "Review Centre", + StoryIntelligencePipelineStages.CharacterReview => "Character Resolution Needed", StoryIntelligencePipelineStages.SceneReview => "Needs Scene Review", - _ when StoryIntelligenceReadyForKnowledge(state) => "Waiting for Knowledge Review", - _ when StoryIntelligenceReadyForRelationships(state) => "Waiting for Relationship Review", - _ when StoryIntelligenceReadyForAssets(state) => "Waiting for Asset Review", - _ when StoryIntelligenceReadyForLocations(state) => "Waiting for Location Review", + _ when StoryIntelligenceReadyForKnowledge(state) => "Review Centre", + _ when StoryIntelligenceReadyForRelationships(state) => "Review Centre", + _ when StoryIntelligenceReadyForAssets(state) => "Review Centre", + _ when StoryIntelligenceReadyForLocations(state) => "Review Centre", _ => string.Equals(state.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase) ? "Story Intelligence In Progress" : "Story Intelligence Needs Review" @@ -287,20 +287,20 @@ public sealed class StoryIntelligenceService( private static string PipelineDescription(StoryIntelligenceBookPipelineState state) => state.IsComplete - ? "All currently available Story Intelligence stages are complete." + ? "Story Intelligence review is saved." : state.CurrentReviewStage switch { - StoryIntelligencePipelineStages.KnowledgeReview => "Review detected knowledge changes and decide what to create or link.", - StoryIntelligencePipelineStages.RelationshipReview => "Review detected relationships and decide what to create or link.", - StoryIntelligencePipelineStages.AssetReview => "Review detected assets and decide what to create or link.", - StoryIntelligencePipelineStages.LocationReview => "Review detected locations and decide what to create or link.", - StoryIntelligencePipelineStages.CharacterReview => "Review detected characters and decide what to create or link.", + StoryIntelligencePipelineStages.KnowledgeReview => "Open the Review Centre and choose which saved category to handle.", + 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 => "Resolve character identities before reviewing dependent story intelligence.", StoryIntelligencePipelineStages.SceneReview => "Review prepared scenes before creating them.", - _ when StoryIntelligenceReadyForKnowledge(state) => "Review detected knowledge changes and decide what to create or link.", - _ when StoryIntelligenceReadyForRelationships(state) => "Review detected relationships and decide what to create or link.", - _ when StoryIntelligenceReadyForAssets(state) => "Review detected assets and decide what to create or link.", - _ when StoryIntelligenceReadyForLocations(state) => "Review detected locations and decide what to create or link.", - _ => "Continue from the next Story Intelligence stage." + _ 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.", + _ when StoryIntelligenceReadyForLocations(state) => "Open the Review Centre and choose which saved category to handle.", + _ => "Open the Review Centre and choose which saved category to handle." }; private static bool StoryIntelligenceReadyForKnowledge(StoryIntelligenceBookPipelineState state) diff --git a/PlotLine/ViewModels/OnboardingViewModels.cs b/PlotLine/ViewModels/OnboardingViewModels.cs index 8f6a4f4..8eb6a0f 100644 --- a/PlotLine/ViewModels/OnboardingViewModels.cs +++ b/PlotLine/ViewModels/OnboardingViewModels.cs @@ -180,6 +180,21 @@ public sealed class StoryIntelligenceProgressViewModel public bool HasRelationshipsToReview => RelationshipReview.Candidates.Count > 0; public bool HasKnowledgeToReview => KnowledgeReview.Candidates.Count > 0; public bool HasNarrativeToReview => NarrativeReview.Candidates.Count > 0; + public int CharacterNeedsAttentionCount => CharacterReview.Candidates.Count; + public int LocationNeedsAttentionCount => LocationReview.Candidates.Count; + public int AssetNeedsAttentionCount => AssetReview.Candidates.Count; + public int RelationshipNeedsAttentionCount => RelationshipReview.Candidates.Count; + public int KnowledgeNeedsAttentionCount => KnowledgeReview.Candidates.Count; + public int NarrativeNeedsAttentionCount => NarrativeReview.Candidates.Count; + public int AnalysisIssueCount => Chapters.Count(chapter => chapter.IsFailed) + Chapters.Sum(chapter => chapter.FailedScenes ?? 0); + public bool CharacterGateSatisfied => PipelineDashboard.CharacterStageComplete || CharacterNeedsAttentionCount == 0; + public int TotalReviewNeedsAttention => CharacterNeedsAttentionCount + + LocationNeedsAttentionCount + + AssetNeedsAttentionCount + + RelationshipNeedsAttentionCount + + KnowledgeNeedsAttentionCount + + NarrativeNeedsAttentionCount + + AnalysisIssueCount; } public sealed class StoryIntelligencePipelineDashboardViewModel diff --git a/PlotLine/Views/Books/Details.cshtml b/PlotLine/Views/Books/Details.cshtml index a331c35..6a4cb65 100644 --- a/PlotLine/Views/Books/Details.cshtml +++ b/PlotLine/Views/Books/Details.cshtml @@ -209,22 +209,22 @@ { if (pipeline.IsComplete) { - return "All currently available Story Intelligence stages are complete."; + return "Story Intelligence review is saved for this book."; } return pipeline.CurrentReviewStage switch { - StoryIntelligencePipelineStages.NarrativeReview => "Review detected Plot Lines and Threads before adding them to the story database.", - StoryIntelligencePipelineStages.RelationshipReview => "Review detected relationships and decide what to create or link.", - StoryIntelligencePipelineStages.AssetReview => "Review detected assets and decide what to create or link.", - StoryIntelligencePipelineStages.LocationReview => "Review detected locations and decide what to create or link.", - StoryIntelligencePipelineStages.CharacterReview => "Scene creation is complete. Review detected characters to continue building the story database.", + StoryIntelligencePipelineStages.NarrativeReview => "Open the Review Centre and choose which saved category to handle.", + 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.", - _ when StoryIntelligenceReadyForNarrative(pipeline) => "Review detected Plot Lines and Threads before adding them to the story database.", - _ when StoryIntelligenceReadyForRelationships(pipeline) => "Review detected relationships and decide what to create or link.", - _ when StoryIntelligenceReadyForAssets(pipeline) => "Review detected assets and decide what to create or link.", - _ when StoryIntelligenceReadyForLocations(pipeline) => "Review detected locations and decide what to create or link.", - _ => "PlotDirector will resume from the next Story Intelligence stage." + _ 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.", + _ when StoryIntelligenceReadyForLocations(pipeline) => "Open the Review Centre and choose which saved category to handle.", + _ => "PlotDirector will open the Review Centre for this book." }; } diff --git a/PlotLine/Views/Onboarding/StoryIntelligence.cshtml b/PlotLine/Views/Onboarding/StoryIntelligence.cshtml index f14cea7..349cd48 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligence.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligence.cshtml @@ -25,9 +25,9 @@ {

Analysis has already started

-

You can continue reviewing progress for the approved chapters.

+

You can return to progress or open the Review Centre when analysis is ready.

- Continue pipeline + View progress
@@ -55,7 +55,7 @@
Review You approve creation -

Scenes are created only after the analysis completes and you choose Create scenes.

+

After analysis, unresolved character identities are handled first, then optional review categories are available later.

@@ -72,9 +72,9 @@
1Analyse chapters
-
2Review scenes
-
3Review story entities
-
4Build database
+
2Create scene intelligence
+
3Resolve characters
+
4Open Review Centre
diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceAssets.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceAssets.cshtml index d382539..41a47a5 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceAssets.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceAssets.cshtml @@ -9,9 +9,8 @@

Phase 3

Review Story Intelligence

-

Stage 4 of 5

Review Assets

-

Approve the important objects PlotDirector should create or link from your imported scenes. Nothing is created unless you approve it.

+

Review important object suggestions that need a create, link, alias, or ignore decision.

@if (TempData["OnboardingStoryIntelligenceError"] is string error) @@ -33,16 +32,16 @@

Assets are already up to date, or there were no story-significant asset suggestions ready to import.

-
Review RelationshipsNext
- @FutureStage("Review Knowledge") - @FutureStage("Review Continuity") - @FutureStage("Timeline Events") + @FutureStage("Relationships") + @FutureStage("Knowledge") + @FutureStage("Plot Lines & Threads") + @FutureStage("Scene Metrics")
- Back to locations -
+ Back to Review Centre + - +
} @@ -56,7 +55,7 @@ -
+
@for (var i = 0; i < Model.AssetReview.Candidates.Count; i++) @@ -145,8 +144,8 @@
- Back to locations - + Back to Review Centre +
} @@ -222,11 +221,12 @@ cards().forEach(updateCard); })(); + } @functions { private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "Not detected" : value; private static Microsoft.AspNetCore.Html.IHtmlContent FutureStage(string label) - => new Microsoft.AspNetCore.Html.HtmlString($"
{System.Net.WebUtility.HtmlEncode(label)}Coming Soon
"); + => new Microsoft.AspNetCore.Html.HtmlString($"
{System.Net.WebUtility.HtmlEncode(label)}Available from Review Centre
"); } diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceCharacters.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceCharacters.cshtml index 81ebb3b..7a1687c 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceCharacters.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceCharacters.cshtml @@ -9,9 +9,8 @@

Phase 3

Review Story Intelligence

-

Stage 2 of 4

Review Characters

-

Approve the characters PlotDirector should create or link from your imported scenes. You stay in control of every character record.

+

Resolve character identity questions before reviewing the rest of the story intelligence.

@if (TempData["OnboardingStoryIntelligenceError"] is string error) @@ -39,14 +38,13 @@

Characters are already up to date, or there were no character suggestions ready to import.

- @FutureStage("Review Locations") - @FutureStage("Review Assets") - @FutureStage("Review Relationships") - @FutureStage("Review Knowledge") + @FutureStage("Locations") + @FutureStage("Assets") + @FutureStage("Relationships") + @FutureStage("Knowledge")
} else @@ -59,7 +57,7 @@ -
+
@for (var i = 0; i < Model.CharacterReview.Candidates.Count; i++) @@ -140,6 +138,7 @@ } @@ -54,7 +53,7 @@
-
+
@for (var i = 0; i < Model.KnowledgeReview.Candidates.Count; i++) @@ -149,8 +148,8 @@
- Back to relationships - + Back to Review Centre +
} @@ -226,11 +225,12 @@ cards().forEach(updateCard); })(); + } @functions { private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "Not detected" : value; private static Microsoft.AspNetCore.Html.IHtmlContent FutureStage(string label) - => new Microsoft.AspNetCore.Html.HtmlString($"
{System.Net.WebUtility.HtmlEncode(label)}Coming Soon
"); + => new Microsoft.AspNetCore.Html.HtmlString($"
{System.Net.WebUtility.HtmlEncode(label)}Available from Review Centre
"); } diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceLocations.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceLocations.cshtml index bb5d19e..1f0b1ce 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceLocations.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceLocations.cshtml @@ -9,9 +9,8 @@

Phase 3

Review Story Intelligence

-

Stage 3 of 4

Review Locations

-

Approve the places PlotDirector should create or link from your imported scenes. Nothing is created unless you approve it.

+

Review place suggestions that need a create, link, alias, or ignore decision.

@if (TempData["OnboardingStoryIntelligenceError"] is string error) @@ -33,14 +32,13 @@

Locations are already up to date, or there were no location suggestions ready to import.

-
Review AssetsNext
- @FutureStage("Review Relationships") - @FutureStage("Review Knowledge") - @FutureStage("Review Continuity") + @FutureStage("Assets") + @FutureStage("Relationships") + @FutureStage("Knowledge") + @FutureStage("Plot Lines & Threads")
} else @@ -53,7 +51,7 @@ -
+
@for (var i = 0; i < Model.LocationReview.Candidates.Count; i++) @@ -134,8 +132,8 @@
- Back to characters - + Back to Review Centre +
} @@ -211,11 +209,12 @@ cards().forEach(updateCard); })(); + } @functions { private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "Not detected" : value; private static Microsoft.AspNetCore.Html.IHtmlContent FutureStage(string label) - => new Microsoft.AspNetCore.Html.HtmlString($"
{System.Net.WebUtility.HtmlEncode(label)}Coming Soon
"); + => new Microsoft.AspNetCore.Html.HtmlString($"
{System.Net.WebUtility.HtmlEncode(label)}Available from Review Centre
"); } diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceNarrative.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceNarrative.cshtml index 5d8bbc0..d8025c9 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceNarrative.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceNarrative.cshtml @@ -9,9 +9,8 @@

Phase 3

Review Story Intelligence

-

Stage 7 of 7

Review Plot Lines & Threads

-

Approve recurring narrative arcs and useful thread candidates before PlotDirector adds them to the canonical story database.

+

Review recurring narrative arcs and thread candidates from the discovered hierarchy.

@if (TempData["OnboardingStoryIntelligenceError"] is string error) @@ -31,15 +30,15 @@ No Plot Line or Thread candidates are waiting.

PlotDirector did not find recurring narrative candidates that require approval, or this review has already been completed.

-
+ - Back to knowledge - + Back to Review Centre +
} else { -
+
@for (var i = 0; i < Model.NarrativeReview.Candidates.Count; i++) @@ -145,10 +144,14 @@
- Back to knowledge - + Back to Review Centre +
}
+ +@section Scripts { + +} diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml index 6ac0b2f..e8b6c2b 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml @@ -150,8 +150,8 @@ { Review scenes + asp-action="@(Model.AllCommitted && Model.CharacterGateSatisfied ? "StoryIntelligenceReviewCentre" : "StoryIntelligenceReview")" + asp-route-batchId="@Model.BatchID">@(Model.AllCommitted && Model.CharacterGateSatisfied ? "Open Review Centre" : "Create scene intelligence") } } diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceRelationships.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceRelationships.cshtml index 31bb125..554792a 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceRelationships.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceRelationships.cshtml @@ -9,9 +9,8 @@

Phase 3

Review Story Intelligence

-

Stage 5 of 5

Review Relationships

-

Approve the character relationships PlotDirector should create or link from your imported scenes. Nothing is created unless you approve it.

+

Review relationship suggestions by character pair and decide what should become canonical.

@if (TempData["OnboardingStoryIntelligenceError"] is string error) @@ -33,15 +32,15 @@

Relationships are already up to date, or there were no relationship suggestions ready to import from the stored scene analysis.

- @FutureStage("Review Knowledge") - @FutureStage("Review Continuity") - @FutureStage("Timeline Events") + @FutureStage("Knowledge") + @FutureStage("Plot Lines & Threads") + @FutureStage("Scene Metrics")
- Back to assets -
+ Back to Review Centre + - +
} @@ -55,7 +54,7 @@ -
+
@for (var i = 0; i < Model.RelationshipReview.Candidates.Count; i++) @@ -160,8 +159,8 @@
- Back to assets - + Back to Review Centre +
} @@ -237,11 +236,12 @@ cards().forEach(updateCard); })(); + } @functions { private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "Not detected" : value; private static Microsoft.AspNetCore.Html.IHtmlContent FutureStage(string label) - => new Microsoft.AspNetCore.Html.HtmlString($"
{System.Net.WebUtility.HtmlEncode(label)}Coming Soon
"); + => new Microsoft.AspNetCore.Html.HtmlString($"
{System.Net.WebUtility.HtmlEncode(label)}Available from Review Centre
"); } diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml index 85aac39..2eda890 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml @@ -2,7 +2,7 @@ @using System.Security.Claims @inject IConfiguration Configuration @{ - ViewData["Title"] = "Review scenes"; + ViewData["Title"] = "Create scene intelligence"; var adminEmails = Configuration.GetSection("Admin:AllowedEmails").Get() ?? []; var userEmail = User.FindFirstValue(ClaimTypes.Email); var isAdmin = !string.IsNullOrWhiteSpace(userEmail) @@ -15,11 +15,10 @@
-

Phase 3

-

Review Story Intelligence

-

Stage 1 of 4

-

Review Scenes

-

PlotDirector has finished reading your manuscript. Review the detected scenes before creating them in your story database.

+

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) @@ -32,7 +31,7 @@ }
- Scene creation runs first. Character import is reviewed separately and only creates or links approved characters. Locations, assets, relationships and knowledge remain for later Story Intelligence phases. + Scene intelligence is created first. Character resolution is reviewed separately; after that, locations, assets, relationships, knowledge, and Plot Lines can be reviewed independently.
@@ -186,20 +185,26 @@ Back @if (Model.HasReadyScenesToCreate) { -
+ - +
} else if (Model.AllCommitted) { - Review characters + @(Model.CharacterGateSatisfied ? "Open Review Centre" : "Resolve characters") } }
+@section Scripts { + +} + @functions { private static bool NeedsReview(StoryIntelligenceOnboardingChapterViewModel chapter) => chapter.IsFailed diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml new file mode 100644 index 0000000..1d8c2e6 --- /dev/null +++ b/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml @@ -0,0 +1,152 @@ +@model StoryIntelligenceProgressViewModel +@{ + ViewData["Title"] = "Story Intelligence Review Centre"; +} + +
+
+ + +
+

Story Intelligence

+

Review Centre

+

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) + { +
@error
+ } + @if (TempData["OnboardingStoryIntelligenceMessage"] is string message) + { +
@message
+ } + + + +
+ @ReviewModule( + "Characters", + Model.CharacterNeedsAttentionCount, + Model.PipelineDashboard.CharacterStageComplete ? "Resolved" : "Needs attention", + "Resolve identity questions before reviewing dependent story intelligence.", + "Review characters", + "StoryIntelligenceCharacters", + false) + + @ReviewModule( + "Locations", + Model.LocationNeedsAttentionCount, + Model.PipelineDashboard.LocationStageComplete ? "Reviewed" : AttentionLabel(Model.LocationNeedsAttentionCount), + "Places found in the manuscript that may be worth creating, linking, or ignoring.", + "Review locations", + "StoryIntelligenceLocations", + !Model.CharacterGateSatisfied) + + @ReviewModule( + "Assets", + Model.AssetNeedsAttentionCount, + Model.PipelineDashboard.AssetStageComplete ? "Reviewed" : AttentionLabel(Model.AssetNeedsAttentionCount), + "Important objects and possessions detected from generic evidence such as recurrence, ownership, and scene prominence.", + "Review assets", + "StoryIntelligenceAssets", + !Model.CharacterGateSatisfied) + + @ReviewModule( + "Relationships", + Model.RelationshipNeedsAttentionCount, + Model.PipelineDashboard.RelationshipStageComplete ? "Reviewed" : AttentionLabel(Model.RelationshipNeedsAttentionCount), + "Relationship candidates grouped by the canonical character identities involved.", + "Review relationships", + "StoryIntelligenceRelationships", + !Model.CharacterGateSatisfied) + + @ReviewModule( + "Knowledge", + Model.KnowledgeNeedsAttentionCount, + Model.PipelineDashboard.KnowledgeStageComplete ? "Reviewed" : AttentionLabel(Model.KnowledgeNeedsAttentionCount), + "Facts, discoveries, beliefs, and suspicions that need a create, link, merge, or ignore decision.", + "Review knowledge", + "StoryIntelligenceKnowledge", + !Model.CharacterGateSatisfied) + + @ReviewModule( + "Plot Lines & Threads", + Model.NarrativeNeedsAttentionCount, + Model.PipelineDashboard.NarrativeStageComplete ? "Reviewed" : AttentionLabel(Model.NarrativeNeedsAttentionCount), + "Recurring arcs and threads organised by the narrative hierarchy discovered from scene evidence.", + "Review plot lines", + "StoryIntelligenceNarrative", + !Model.CharacterGateSatisfied) + +
+
+

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.AnalysisIssueCount > 0) + { +
+
+

Analysis Issues

+

Some chapters or scenes reported analysis issues. Canonical chapters and scenes remain intact.

+
+ Needs attention + View issues + @Model.AnalysisIssueCount.ToString("N0") need attention +
+ } +
+ + +
+
+ +@functions { + private string AttentionLabel(int count) => count == 0 ? "No outstanding decisions" : "Needs attention"; + + private Microsoft.AspNetCore.Html.IHtmlContent ReviewModule( + string title, + int count, + string status, + string description, + string actionLabel, + string action, + bool blocked) + { + var statusClass = blocked + ? "story-review-status story-review-status--active" + : 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 countText = blocked + ? "Resolve characters first" + : count == 1 + ? "1 needs attention" + : $"{count:N0} need attention"; + var disabled = blocked ? " disabled aria-disabled=\"true\"" : string.Empty; + var href = blocked + ? "#" + : Url.Action(action, "Onboarding", new { batchId = Model.BatchID }) ?? "#"; + + return new Microsoft.AspNetCore.Html.HtmlString($$""" +
+
+

{{System.Net.WebUtility.HtmlEncode(title)}}

+

{{System.Net.WebUtility.HtmlEncode(description)}}

+
+ {{System.Net.WebUtility.HtmlEncode(statusText)}} + {{System.Net.WebUtility.HtmlEncode(actionLabel)}} + {{System.Net.WebUtility.HtmlEncode(countText)}} +
+ """); + } +} diff --git a/PlotLine/Views/Onboarding/_StoryIntelligencePipelineHeader.cshtml b/PlotLine/Views/Onboarding/_StoryIntelligencePipelineHeader.cshtml index 263d6e4..2685206 100644 --- a/PlotLine/Views/Onboarding/_StoryIntelligencePipelineHeader.cshtml +++ b/PlotLine/Views/Onboarding/_StoryIntelligencePipelineHeader.cshtml @@ -1,130 +1,67 @@ @model StoryIntelligencePipelineHeaderViewModel @{ - var reviewStages = new[] - { - new PipelineStage("Review Scenes", "Scenes", true), - new PipelineStage("Review Characters", "Characters", true), - new PipelineStage("Review Locations", "Locations", true), - new PipelineStage("Review Assets", "Assets", true), - new PipelineStage("Review Relationships", "Relationships", true), - new PipelineStage("Review Knowledge", "Knowledge", true) - }; - var implementedReviewStages = reviewStages.Where(stage => stage.IsImplemented).ToList(); - var currentReviewIndex = implementedReviewStages.FindIndex(stage => string.Equals(stage.StageName, Model.CurrentStage, StringComparison.OrdinalIgnoreCase)); - var isReviewPhase = currentReviewIndex >= 0; - var isComplete = string.Equals(Model.CurrentStage, "Complete", StringComparison.OrdinalIgnoreCase); var isAnalyse = string.Equals(Model.CurrentStage, "Analyse Manuscript", StringComparison.OrdinalIgnoreCase); - var activePhase = isComplete ? 4 : isReviewPhase ? 3 : isAnalyse ? 2 : 1; - var progressPercent = isReviewPhase - ? Math.Clamp(((currentReviewIndex + 1) * 100) / implementedReviewStages.Count, 0, 100) - : isComplete ? 100 : 100; - var currentStageNumber = isReviewPhase ? currentReviewIndex + 1 : isComplete ? implementedReviewStages.Count : 0; + var isSceneImport = string.Equals(Model.CurrentStage, "Review Scenes", StringComparison.OrdinalIgnoreCase); + var isReviewCentre = string.Equals(Model.CurrentStage, "Review Centre", StringComparison.OrdinalIgnoreCase); + var phase = isAnalyse ? 2 : isSceneImport ? 2 : 3; + var progressPercent = isAnalyse ? 50 : isSceneImport ? 75 : 100; } -
+
-

Phase @activePhase

-

@CurrentPhaseTitle(activePhase)

- @if (isReviewPhase) - { -

Stage @currentStageNumber of @implementedReviewStages.Count

- } - else if (isComplete) - { -

Final import and completion

- } - else if (isAnalyse) - { -

Story Intelligence Analysis

- } - else - { -

Prepare the manuscript for analysis

- } +

Phase @phase

+

@CurrentPhaseTitle()

+

@CurrentPhaseDescription()

-
- @ProgressLabel(activePhase, Model.CurrentStage) +
+ @ProgressLabel()
    -
  1. - Prepare Manuscript - 7 completed steps -
      -
    • Welcome
    • -
    • Writing Preferences
    • -
    • Project
    • -
    • Book
    • -
    • Connect Word
    • -
    • Scan Manuscript
    • -
    • Review Chapters
    • -
    +
  2. + Core Import + Book structure created from Word
  3. -
  4. +
  5. Analyse Manuscript - Story Intelligence Analysis + Scene intelligence saved to PlotDirector
  6. -
  7. - Review Story Intelligence - @ReviewSummary(isReviewPhase, currentStageNumber, implementedReviewStages.Count) -
      - @foreach (var stage in reviewStages) - { - var index = implementedReviewStages.FindIndex(item => string.Equals(item.StageName, stage.StageName, StringComparison.OrdinalIgnoreCase)); - var stageClass = !stage.IsImplemented - ? "is-upcoming" - : isComplete || index < currentReviewIndex - ? "is-complete" - : index == currentReviewIndex - ? "is-current" - : "is-upcoming"; -
    • - - @stage.Label - @if (!stage.IsImplemented) - { - Coming Soon - } -
    • - } -
    -
  8. -
  9. - Build Story Database - Final import and completion +
  10. + Review Centre + Independent optional review categories
@functions { - private sealed record PipelineStage(string StageName, string Label, bool IsImplemented); - - private static string CurrentPhaseTitle(int phase) - => phase switch + private string CurrentPhaseTitle() + => Model.CurrentStage switch { - 1 => "Prepare Manuscript", - 2 => "Analyse Manuscript", - 3 => "Review Story Intelligence", - _ => "Build Story Database" + "Analyse Manuscript" => "Analyse Manuscript", + "Review Scenes" => "Create Scene Intelligence", + "Review Centre" => "Review Centre", + _ => "Review Story Intelligence" }; - private static string ProgressLabel(int phase, string currentStage) - => phase switch + private string CurrentPhaseDescription() + => Model.CurrentStage switch { - 3 => currentStage, - 4 => "Finalising import", - _ => "Completed" + "Analyse Manuscript" => "PlotDirector is reading saved canonical scenes.", + "Review Scenes" => "Create the stored scene intelligence before opening review categories.", + "Review Centre" => "Choose the review category you want to handle now.", + _ => "This category can be reviewed independently after character resolution." }; - private static string ReviewSummary(bool isReviewPhase, int currentStageNumber, int implementedStageCount) - => isReviewPhase - ? $"{Math.Max(0, currentStageNumber - 1)} of {implementedStageCount} review stages completed" - : $"{implementedStageCount} review stages completed"; - - private static string StageMarker(string stageClass, bool isImplemented) - => !isImplemented ? "○" : stageClass == "is-current" ? "►" : stageClass == "is-complete" ? "✓" : "○"; + private string ProgressLabel() + => Model.CurrentStage switch + { + "Analyse Manuscript" => "Analysis running", + "Review Scenes" => "Scene intelligence", + "Review Centre" => "Review Centre", + _ => Model.CurrentStage + }; } diff --git a/PlotLine/Views/Onboarding/_StoryIntelligencePipelineSummary.cshtml b/PlotLine/Views/Onboarding/_StoryIntelligencePipelineSummary.cshtml index 7e276eb..1f7c5ca 100644 --- a/PlotLine/Views/Onboarding/_StoryIntelligencePipelineSummary.cshtml +++ b/PlotLine/Views/Onboarding/_StoryIntelligencePipelineSummary.cshtml @@ -1,60 +1,44 @@ @model StoryIntelligencePipelineDashboardViewModel -
+
Chapters analysed - ✓ @Model.ChaptersAnalysed.ToString("N0") + @Model.ChaptersAnalysed.ToString("N0")
Scenes imported - ✓ @Model.ScenesImported.ToString("N0") + @Model.ScenesImported.ToString("N0")
- Characters identified - @Model.CharactersIdentified.ToString("N0") + Characters + @(Model.CharacterStageComplete ? "Resolved" : "Needs attention")
- Characters created / linked - @(Model.CharacterStageComplete ? $"Done: {Model.CharactersCreatedOrLinked:N0}" : "In review") + Locations + @StageStatus(Model.LocationStageComplete)
- Locations identified - @Model.LocationsIdentified.ToString("N0") + Assets + @StageStatus(Model.AssetStageComplete)
- Locations created / linked - @(Model.LocationStageComplete ? $"Done: {Model.LocationsCreatedOrLinked:N0}" : "In review") + Relationships + @StageStatus(Model.RelationshipStageComplete)
- Assets identified - @Model.AssetsIdentified.ToString("N0") + Knowledge + @StageStatus(Model.KnowledgeStageComplete)
- Assets created / linked - @(Model.AssetStageComplete ? $"Done: {Model.AssetsCreatedOrLinked:N0}" : "In review") + Plot Lines & Threads + @StageStatus(Model.NarrativeStageComplete)
- Relationships identified - @Model.RelationshipsIdentified.ToString("N0") -
-
- Relationships created / linked - @(Model.RelationshipStageComplete ? $"Done: {Model.RelationshipsCreatedOrLinked:N0}" : "In review") -
-
- Knowledge identified - @Model.KnowledgeIdentified.ToString("N0") -
-
- Knowledge created / linked - @(Model.KnowledgeStageComplete ? $"Done: {Model.KnowledgeCreatedOrLinked:N0}" : "In review") -
-
- Plot Lines / Threads found - @Model.NarrativeCandidatesIdentified.ToString("N0") -
-
- Plot Lines / Threads applied - @(Model.NarrativeStageComplete ? $"Done: {Model.NarrativeCandidatesApplied:N0}" : "In review") + Scene metrics + Available
+ +@functions { + private static string StageStatus(bool complete) => complete ? "Reviewed" : "Optional"; +} diff --git a/PlotLine/wwwroot/css/onboarding.css b/PlotLine/wwwroot/css/onboarding.css index 9043a19..62aa7f9 100644 --- a/PlotLine/wwwroot/css/onboarding.css +++ b/PlotLine/wwwroot/css/onboarding.css @@ -1384,6 +1384,38 @@ summary.story-review-chapter-heading { margin-top: .2rem; } +.story-review-centre-list { + display: grid; + gap: .65rem; +} + +.story-review-centre-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + gap: .75rem 1rem; + align-items: center; + border: 1px solid rgba(31, 42, 68, .1); + border-radius: 8px; + padding: .85rem 1rem; + background: rgba(255, 255, 255, .64); +} + +.story-review-centre-row h2 { + margin: 0; + font-size: 1.05rem; +} + +.story-review-centre-row p { + margin: .2rem 0 0; + color: var(--bs-secondary-color); +} + +.story-review-centre-row__count { + grid-column: 1 / -1; + color: var(--bs-secondary-color); + font-weight: 700; +} + .story-scene-preview-list { display: grid; gap: .65rem; @@ -2010,6 +2042,7 @@ summary.story-review-chapter-heading { .story-review-summary, .story-review-scene-panel, .story-character-card__body dl, + .story-review-centre-row, .story-future-stage-grid, .story-scene-preview dl, .story-live-current, diff --git a/PlotLine/wwwroot/js/story-review-actions.js b/PlotLine/wwwroot/js/story-review-actions.js new file mode 100644 index 0000000..b42740c --- /dev/null +++ b/PlotLine/wwwroot/js/story-review-actions.js @@ -0,0 +1,19 @@ +(function () { + const forms = document.querySelectorAll("[data-story-review-form]"); + forms.forEach((form) => { + form.addEventListener("submit", (event) => { + if (form.dataset.submitting === "true") { + event.preventDefault(); + return; + } + + form.dataset.submitting = "true"; + form.querySelectorAll("button[type='submit']").forEach((button) => { + button.dataset.originalLabel = button.textContent.trim(); + button.textContent = button.dataset.busyLabel || "Saving..."; + button.disabled = true; + button.setAttribute("aria-busy", "true"); + }); + }); + }); +})();