Add story intelligence review centre
This commit is contained in:
parent
90d8f65fcf
commit
da955db9d1
@ -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<IActionResult> StoryIntelligenceLocations", "[HttpGet(\"story-intelligence/assets\")]");
|
||||
var assetsAction = ExtractBetween(controller, "public async Task<IActionResult> StoryIntelligenceAssets", "[HttpGet(\"story-intelligence/relationships\")]");
|
||||
var relationshipsAction = ExtractBetween(controller, "public async Task<IActionResult> StoryIntelligenceRelationships", "[HttpPost(\"story-intelligence/cancel\")]");
|
||||
var knowledgeAction = ExtractBetween(controller, "public async Task<IActionResult> StoryIntelligenceKnowledge", "[HttpPost(\"story-intelligence/knowledge\")]");
|
||||
var narrativeAction = ExtractBetween(controller, "public async Task<IActionResult> 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<IActionResult> 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");
|
||||
|
||||
@ -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<IActionResult> 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<IActionResult> 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);
|
||||
}
|
||||
|
||||
@ -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."
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -25,9 +25,9 @@
|
||||
{
|
||||
<section class="onboarding-story-card">
|
||||
<h2>Analysis has already started</h2>
|
||||
<p>You can continue reviewing progress for the approved chapters.</p>
|
||||
<p>You can return to progress or open the Review Centre when analysis is ready.</p>
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.ExistingBatchID">Continue pipeline</a>
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceProgress" asp-route-batchId="@Model.ExistingBatchID">View progress</a>
|
||||
<form asp-action="ScanManuscript" method="post">
|
||||
<button class="btn btn-outline-secondary" type="submit">Scan manuscript again</button>
|
||||
</form>
|
||||
@ -55,7 +55,7 @@
|
||||
<div>
|
||||
<span>Review</span>
|
||||
<strong>You approve creation</strong>
|
||||
<p>Scenes are created only after the analysis completes and you choose Create scenes.</p>
|
||||
<p>After analysis, unresolved character identities are handled first, then optional review categories are available later.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -72,9 +72,9 @@
|
||||
|
||||
<div class="onboarding-analysis-flow">
|
||||
<div><span>1</span><strong>Analyse chapters</strong></div>
|
||||
<div><span>2</span><strong>Review scenes</strong></div>
|
||||
<div><span>3</span><strong>Review story entities</strong></div>
|
||||
<div><span>4</span><strong>Build database</strong></div>
|
||||
<div><span>2</span><strong>Create scene intelligence</strong></div>
|
||||
<div><span>3</span><strong>Resolve characters</strong></div>
|
||||
<div><span>4</span><strong>Open Review Centre</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
|
||||
@ -9,9 +9,8 @@
|
||||
<div class="onboarding-copy story-review-heading">
|
||||
<p class="eyebrow">Phase 3</p>
|
||||
<p class="story-review-heading__phase">Review Story Intelligence</p>
|
||||
<p class="story-review-heading__stage">Stage 4 of 5</p>
|
||||
<h1 id="story-asset-title">Review Assets</h1>
|
||||
<p>Approve the important objects PlotDirector should create or link from your imported scenes. Nothing is created unless you approve it.</p>
|
||||
<p>Review important object suggestions that need a create, link, alias, or ignore decision.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
@ -33,16 +32,16 @@
|
||||
<p class="mb-0">Assets are already up to date, or there were no story-significant asset suggestions ready to import.</p>
|
||||
</div>
|
||||
<div class="story-future-stage-grid">
|
||||
<article class="story-future-stage"><strong>Review Relationships</strong><span>Next</span></article>
|
||||
@FutureStage("Review Knowledge")
|
||||
@FutureStage("Review Continuity")
|
||||
@FutureStage("Timeline Events")
|
||||
@FutureStage("Relationships")
|
||||
@FutureStage("Knowledge")
|
||||
@FutureStage("Plot Lines & Threads")
|
||||
@FutureStage("Scene Metrics")
|
||||
</div>
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceLocations" asp-route-batchId="@Model.BatchID">Back to locations</a>
|
||||
<form asp-action="ImportStoryIntelligenceAssets" method="post">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
<form asp-action="ImportStoryIntelligenceAssets" method="post" data-story-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<button class="btn btn-primary" type="submit">Continue</button>
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Saving assets...">Mark reviewed</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
@ -56,7 +55,7 @@
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-asset-bulk="collapse">Collapse all</button>
|
||||
</div>
|
||||
|
||||
<form asp-action="ImportStoryIntelligenceAssets" method="post" data-asset-review-form>
|
||||
<form asp-action="ImportStoryIntelligenceAssets" method="post" data-asset-review-form data-story-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<div class="story-character-card-grid">
|
||||
@for (var i = 0; i < Model.AssetReview.Candidates.Count; i++)
|
||||
@ -145,8 +144,8 @@
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceLocations" asp-route-batchId="@Model.BatchID">Back to locations</a>
|
||||
<button class="btn btn-primary" type="submit">Create assets</button>
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Saving assets...">Save asset decisions</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
@ -222,11 +221,12 @@
|
||||
cards().forEach(updateCard);
|
||||
})();
|
||||
</script>
|
||||
<script src="~/js/story-review-actions.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
@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($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Coming Soon</span></article>");
|
||||
=> new Microsoft.AspNetCore.Html.HtmlString($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Available from Review Centre</span></article>");
|
||||
}
|
||||
|
||||
@ -9,9 +9,8 @@
|
||||
<div class="onboarding-copy story-review-heading">
|
||||
<p class="eyebrow">Phase 3</p>
|
||||
<p class="story-review-heading__phase">Review Story Intelligence</p>
|
||||
<p class="story-review-heading__stage">Stage 2 of 4</p>
|
||||
<h1 id="story-character-title">Review Characters</h1>
|
||||
<p>Approve the characters PlotDirector should create or link from your imported scenes. You stay in control of every character record.</p>
|
||||
<p>Resolve character identity questions before reviewing the rest of the story intelligence.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
@ -39,14 +38,13 @@
|
||||
<p class="mb-0">Characters are already up to date, or there were no character suggestions ready to import.</p>
|
||||
</div>
|
||||
<div class="story-future-stage-grid">
|
||||
@FutureStage("Review Locations")
|
||||
@FutureStage("Review Assets")
|
||||
@FutureStage("Review Relationships")
|
||||
@FutureStage("Review Knowledge")
|
||||
@FutureStage("Locations")
|
||||
@FutureStage("Assets")
|
||||
@FutureStage("Relationships")
|
||||
@FutureStage("Knowledge")
|
||||
</div>
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.BatchID">Back to scenes</a>
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceLocations" asp-route-batchId="@Model.BatchID">Continue</a>
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@ -59,7 +57,7 @@
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-character-bulk="collapse">Collapse all</button>
|
||||
</div>
|
||||
|
||||
<form asp-action="ImportStoryIntelligenceCharacters" method="post" data-character-review-form>
|
||||
<form asp-action="ImportStoryIntelligenceCharacters" method="post" data-character-review-form data-story-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<div class="story-character-card-grid">
|
||||
@for (var i = 0; i < Model.CharacterReview.Candidates.Count; i++)
|
||||
@ -140,6 +138,7 @@
|
||||
|
||||
<div data-character-alias-panel hidden>
|
||||
<label class="form-label" for="character-alias-target-@i">Alias target</label>
|
||||
<p class="form-text">Treats both identities as the same physical character and keeps this manuscript name as an alias/reference.</p>
|
||||
<select id="character-alias-target-@i" class="form-select" name="Characters[@i].AliasTargetCharacterID" data-character-alias-target>
|
||||
<option value="">Choose character...</option>
|
||||
@foreach (var target in Model.CharacterReview.AliasTargetOptions)
|
||||
@ -154,8 +153,8 @@
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.BatchID">Back to scenes</a>
|
||||
<button class="btn btn-primary" type="submit">Create characters</button>
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Saving characters...">Save character decisions</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
@ -237,6 +236,7 @@
|
||||
cards().forEach(updateCard);
|
||||
})();
|
||||
</script>
|
||||
<script src="~/js/story-review-actions.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
@functions {
|
||||
@ -246,5 +246,5 @@
|
||||
=> string.Equals(candidate.DefaultAction, action, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static Microsoft.AspNetCore.Html.IHtmlContent FutureStage(string label)
|
||||
=> new Microsoft.AspNetCore.Html.HtmlString($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Coming Soon</span></article>");
|
||||
=> new Microsoft.AspNetCore.Html.HtmlString($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Available from Review Centre</span></article>");
|
||||
}
|
||||
|
||||
@ -21,9 +21,9 @@
|
||||
{
|
||||
<section class="onboarding-story-card">
|
||||
<h2>Analysis has already started</h2>
|
||||
<p>You can continue reviewing progress for the approved chapters.</p>
|
||||
<p>You can return to progress or open the Review Centre when analysis is ready.</p>
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.ExistingBatchID">Continue pipeline</a>
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceProgress" asp-route-batchId="@Model.ExistingBatchID">View progress</a>
|
||||
<form asp-action="ScanManuscript" method="post">
|
||||
<button class="btn btn-outline-secondary" type="submit">Scan manuscript again</button>
|
||||
</form>
|
||||
|
||||
@ -9,9 +9,8 @@
|
||||
<div class="onboarding-copy story-review-heading">
|
||||
<p class="eyebrow">Phase 3</p>
|
||||
<p class="story-review-heading__phase">Review Story Intelligence</p>
|
||||
<p class="story-review-heading__stage">Stage 6 of 6</p>
|
||||
<h1 id="story-knowledge-title">Review Knowledge</h1>
|
||||
<p>Approve the facts, discoveries, beliefs and suspicions PlotDirector should add to the existing Knowledge Explorer.</p>
|
||||
<p>Review facts, discoveries, beliefs, and suspicions that need an author decision.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
@ -33,14 +32,14 @@
|
||||
<p class="mb-0">Knowledge is already up to date, or there were no knowledge changes ready to import from the stored scene analysis.</p>
|
||||
</div>
|
||||
<div class="story-future-stage-grid">
|
||||
@FutureStage("Review Continuity")
|
||||
@FutureStage("Timeline Events")
|
||||
@FutureStage("Plot Lines & Threads")
|
||||
@FutureStage("Scene Metrics")
|
||||
</div>
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceRelationships" asp-route-batchId="@Model.BatchID">Back to relationships</a>
|
||||
<form asp-action="ImportStoryIntelligenceKnowledge" method="post">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
<form asp-action="ImportStoryIntelligenceKnowledge" method="post" data-story-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<button class="btn btn-primary" type="submit">Continue</button>
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Saving knowledge...">Mark reviewed</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
@ -54,7 +53,7 @@
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-knowledge-bulk="collapse">Collapse all</button>
|
||||
</div>
|
||||
|
||||
<form asp-action="ImportStoryIntelligenceKnowledge" method="post" data-knowledge-review-form>
|
||||
<form asp-action="ImportStoryIntelligenceKnowledge" method="post" data-knowledge-review-form data-story-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<div class="story-character-card-grid">
|
||||
@for (var i = 0; i < Model.KnowledgeReview.Candidates.Count; i++)
|
||||
@ -149,8 +148,8 @@
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceRelationships" asp-route-batchId="@Model.BatchID">Back to relationships</a>
|
||||
<button class="btn btn-primary" type="submit">Create knowledge</button>
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Saving knowledge...">Save knowledge decisions</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
@ -226,11 +225,12 @@
|
||||
cards().forEach(updateCard);
|
||||
})();
|
||||
</script>
|
||||
<script src="~/js/story-review-actions.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
@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($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Coming Soon</span></article>");
|
||||
=> new Microsoft.AspNetCore.Html.HtmlString($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Available from Review Centre</span></article>");
|
||||
}
|
||||
|
||||
@ -9,9 +9,8 @@
|
||||
<div class="onboarding-copy story-review-heading">
|
||||
<p class="eyebrow">Phase 3</p>
|
||||
<p class="story-review-heading__phase">Review Story Intelligence</p>
|
||||
<p class="story-review-heading__stage">Stage 3 of 4</p>
|
||||
<h1 id="story-location-title">Review Locations</h1>
|
||||
<p>Approve the places PlotDirector should create or link from your imported scenes. Nothing is created unless you approve it.</p>
|
||||
<p>Review place suggestions that need a create, link, alias, or ignore decision.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
@ -33,14 +32,13 @@
|
||||
<p class="mb-0">Locations are already up to date, or there were no location suggestions ready to import.</p>
|
||||
</div>
|
||||
<div class="story-future-stage-grid">
|
||||
<article class="story-future-stage"><strong>Review Assets</strong><span>Next</span></article>
|
||||
@FutureStage("Review Relationships")
|
||||
@FutureStage("Review Knowledge")
|
||||
@FutureStage("Review Continuity")
|
||||
@FutureStage("Assets")
|
||||
@FutureStage("Relationships")
|
||||
@FutureStage("Knowledge")
|
||||
@FutureStage("Plot Lines & Threads")
|
||||
</div>
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceCharacters" asp-route-batchId="@Model.BatchID">Back to characters</a>
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceAssets" asp-route-batchId="@Model.BatchID">Continue</a>
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@ -53,7 +51,7 @@
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-location-bulk="collapse">Collapse all</button>
|
||||
</div>
|
||||
|
||||
<form asp-action="ImportStoryIntelligenceLocations" method="post" data-location-review-form>
|
||||
<form asp-action="ImportStoryIntelligenceLocations" method="post" data-location-review-form data-story-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<div class="story-character-card-grid">
|
||||
@for (var i = 0; i < Model.LocationReview.Candidates.Count; i++)
|
||||
@ -134,8 +132,8 @@
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceCharacters" asp-route-batchId="@Model.BatchID">Back to characters</a>
|
||||
<button class="btn btn-primary" type="submit">Create locations</button>
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Saving locations...">Save location decisions</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
@ -211,11 +209,12 @@
|
||||
cards().forEach(updateCard);
|
||||
})();
|
||||
</script>
|
||||
<script src="~/js/story-review-actions.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
@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($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Coming Soon</span></article>");
|
||||
=> new Microsoft.AspNetCore.Html.HtmlString($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Available from Review Centre</span></article>");
|
||||
}
|
||||
|
||||
@ -9,9 +9,8 @@
|
||||
<div class="onboarding-copy story-review-heading">
|
||||
<p class="eyebrow">Phase 3</p>
|
||||
<p class="story-review-heading__phase">Review Story Intelligence</p>
|
||||
<p class="story-review-heading__stage">Stage 7 of 7</p>
|
||||
<h1 id="story-narrative-title">Review Plot Lines & Threads</h1>
|
||||
<p>Approve recurring narrative arcs and useful thread candidates before PlotDirector adds them to the canonical story database.</p>
|
||||
<p>Review recurring narrative arcs and thread candidates from the discovered hierarchy.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
@ -31,15 +30,15 @@
|
||||
<strong>No Plot Line or Thread candidates are waiting.</strong>
|
||||
<p class="mb-0">PlotDirector did not find recurring narrative candidates that require approval, or this review has already been completed.</p>
|
||||
</div>
|
||||
<form asp-action="ImportStoryIntelligenceNarrative" method="post" class="onboarding-actions">
|
||||
<form asp-action="ImportStoryIntelligenceNarrative" method="post" class="onboarding-actions" data-story-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceKnowledge" asp-route-batchId="@Model.BatchID">Back to knowledge</a>
|
||||
<button class="btn btn-primary" type="submit">Complete narrative review</button>
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Saving plot lines...">Mark reviewed</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form asp-action="ImportStoryIntelligenceNarrative" method="post">
|
||||
<form asp-action="ImportStoryIntelligenceNarrative" method="post" data-story-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<div class="story-character-card-grid">
|
||||
@for (var i = 0; i < Model.NarrativeReview.Candidates.Count; i++)
|
||||
@ -145,10 +144,14 @@
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceKnowledge" asp-route-batchId="@Model.BatchID">Back to knowledge</a>
|
||||
<button class="btn btn-primary" type="submit">Apply Plot Lines & Threads</button>
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Saving plot lines...">Save Plot Lines & Threads</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/js/story-review-actions.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
@ -150,8 +150,8 @@
|
||||
{
|
||||
<a class="btn btn-primary"
|
||||
data-story-review-link
|
||||
asp-action="StoryIntelligenceReview"
|
||||
asp-route-batchId="@Model.BatchID">Review scenes</a>
|
||||
asp-action="@(Model.AllCommitted && Model.CharacterGateSatisfied ? "StoryIntelligenceReviewCentre" : "StoryIntelligenceReview")"
|
||||
asp-route-batchId="@Model.BatchID">@(Model.AllCommitted && Model.CharacterGateSatisfied ? "Open Review Centre" : "Create scene intelligence")</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -9,9 +9,8 @@
|
||||
<div class="onboarding-copy story-review-heading">
|
||||
<p class="eyebrow">Phase 3</p>
|
||||
<p class="story-review-heading__phase">Review Story Intelligence</p>
|
||||
<p class="story-review-heading__stage">Stage 5 of 5</p>
|
||||
<h1 id="story-relationship-title">Review Relationships</h1>
|
||||
<p>Approve the character relationships PlotDirector should create or link from your imported scenes. Nothing is created unless you approve it.</p>
|
||||
<p>Review relationship suggestions by character pair and decide what should become canonical.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
@ -33,15 +32,15 @@
|
||||
<p class="mb-0">Relationships are already up to date, or there were no relationship suggestions ready to import from the stored scene analysis.</p>
|
||||
</div>
|
||||
<div class="story-future-stage-grid">
|
||||
@FutureStage("Review Knowledge")
|
||||
@FutureStage("Review Continuity")
|
||||
@FutureStage("Timeline Events")
|
||||
@FutureStage("Knowledge")
|
||||
@FutureStage("Plot Lines & Threads")
|
||||
@FutureStage("Scene Metrics")
|
||||
</div>
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceAssets" asp-route-batchId="@Model.BatchID">Back to assets</a>
|
||||
<form asp-action="ImportStoryIntelligenceRelationships" method="post">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
<form asp-action="ImportStoryIntelligenceRelationships" method="post" data-story-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<button class="btn btn-primary" type="submit">Continue</button>
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Saving relationships...">Mark reviewed</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
@ -55,7 +54,7 @@
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-relationship-bulk="collapse">Collapse all</button>
|
||||
</div>
|
||||
|
||||
<form asp-action="ImportStoryIntelligenceRelationships" method="post" data-relationship-review-form>
|
||||
<form asp-action="ImportStoryIntelligenceRelationships" method="post" data-relationship-review-form data-story-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<div class="story-character-card-grid">
|
||||
@for (var i = 0; i < Model.RelationshipReview.Candidates.Count; i++)
|
||||
@ -160,8 +159,8 @@
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceAssets" asp-route-batchId="@Model.BatchID">Back to assets</a>
|
||||
<button class="btn btn-primary" type="submit">Create relationships</button>
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Saving relationships...">Save relationship decisions</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
@ -237,11 +236,12 @@
|
||||
cards().forEach(updateCard);
|
||||
})();
|
||||
</script>
|
||||
<script src="~/js/story-review-actions.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
@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($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Coming Soon</span></article>");
|
||||
=> new Microsoft.AspNetCore.Html.HtmlString($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Available from Review Centre</span></article>");
|
||||
}
|
||||
|
||||
@ -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<string[]>() ?? [];
|
||||
var userEmail = User.FindFirstValue(ClaimTypes.Email);
|
||||
var isAdmin = !string.IsNullOrWhiteSpace(userEmail)
|
||||
@ -15,11 +15,10 @@
|
||||
<div class="onboarding-panel onboarding-review-panel story-review-page">
|
||||
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Review Scenes" })" />
|
||||
<div class="onboarding-copy story-review-heading">
|
||||
<p class="eyebrow">Phase 3</p>
|
||||
<p class="story-review-heading__phase">Review Story Intelligence</p>
|
||||
<p class="story-review-heading__stage">Stage 1 of 4</p>
|
||||
<h1 id="story-review-title">Review Scenes</h1>
|
||||
<p>PlotDirector has finished reading your manuscript. Review the detected scenes before creating them in your story database.</p>
|
||||
<p class="eyebrow">Phase 2</p>
|
||||
<p class="story-review-heading__phase">Create Scene Intelligence</p>
|
||||
<h1 id="story-review-title">Create Scene Intelligence</h1>
|
||||
<p>PlotDirector has finished reading your manuscript. Create the stored scene intelligence before opening the Review Centre.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
@ -32,7 +31,7 @@
|
||||
}
|
||||
|
||||
<section class="alert alert-info">
|
||||
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.
|
||||
</section>
|
||||
|
||||
<section class="story-review-summary" aria-label="Analysis summary">
|
||||
@ -186,20 +185,26 @@
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceProgress" asp-route-batchId="@Model.BatchID">Back</a>
|
||||
@if (Model.HasReadyScenesToCreate)
|
||||
{
|
||||
<form asp-action="CommitAllStoryIntelligence" method="post">
|
||||
<form asp-action="CommitAllStoryIntelligence" method="post" data-story-review-form>
|
||||
<input type="hidden" name="batchId" value="@Model.BatchID" />
|
||||
<button class="btn btn-primary" type="submit">Create PlotDirector Scenes</button>
|
||||
<button class="btn btn-primary" type="submit" data-busy-label="Creating scene intelligence...">Create scene intelligence</button>
|
||||
</form>
|
||||
}
|
||||
else if (Model.AllCommitted)
|
||||
{
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceCharacters" asp-route-batchId="@Model.BatchID">Review characters</a>
|
||||
<a class="btn btn-primary"
|
||||
asp-action="@(Model.CharacterGateSatisfied ? "StoryIntelligenceReviewCentre" : "StoryIntelligenceCharacters")"
|
||||
asp-route-batchId="@Model.BatchID">@(Model.CharacterGateSatisfied ? "Open Review Centre" : "Resolve characters")</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/js/story-review-actions.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
@functions {
|
||||
private static bool NeedsReview(StoryIntelligenceOnboardingChapterViewModel chapter)
|
||||
=> chapter.IsFailed
|
||||
|
||||
152
PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml
Normal file
152
PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml
Normal file
@ -0,0 +1,152 @@
|
||||
@model StoryIntelligenceProgressViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Story Intelligence Review Centre";
|
||||
}
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="story-review-centre-title">
|
||||
<div class="onboarding-panel onboarding-review-panel story-review-page">
|
||||
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Review Centre" })" />
|
||||
|
||||
<div class="onboarding-copy story-review-heading">
|
||||
<p class="eyebrow">Story Intelligence</p>
|
||||
<h1 id="story-review-centre-title">Review Centre</h1>
|
||||
<p>Review only the story intelligence that needs an author decision. Everything here is saved and can be picked up later.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
{
|
||||
<div class="alert alert-danger">@error</div>
|
||||
}
|
||||
@if (TempData["OnboardingStoryIntelligenceMessage"] is string message)
|
||||
{
|
||||
<div class="alert alert-success">@message</div>
|
||||
}
|
||||
|
||||
<partial name="_StoryIntelligencePipelineSummary" model="Model.PipelineDashboard" />
|
||||
|
||||
<section class="story-review-centre-list" aria-label="Story Intelligence review categories">
|
||||
@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)
|
||||
|
||||
<article class="story-review-centre-row">
|
||||
<div>
|
||||
<h2>Scene Metrics</h2>
|
||||
<p>Available from the imported scene analysis. Metrics are auto-applied where configured and do not require a mandatory review step.</p>
|
||||
</div>
|
||||
<strong class="story-review-status story-review-status--ready">Available</strong>
|
||||
<span class="story-review-centre-row__count">0 need attention</span>
|
||||
</article>
|
||||
|
||||
@if (Model.AnalysisIssueCount > 0)
|
||||
{
|
||||
<article class="story-review-centre-row">
|
||||
<div>
|
||||
<h2>Analysis Issues</h2>
|
||||
<p>Some chapters or scenes reported analysis issues. Canonical chapters and scenes remain intact.</p>
|
||||
</div>
|
||||
<strong class="story-review-status story-review-status--warning">Needs attention</strong>
|
||||
<a class="btn btn-outline-primary btn-sm" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.BatchID">View issues</a>
|
||||
<span class="story-review-centre-row__count">@Model.AnalysisIssueCount.ToString("N0") need attention</span>
|
||||
</article>
|
||||
}
|
||||
</section>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-controller="Books" asp-action="Details" asp-route-id="@Model.BookID">Return to book</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="Writer" asp-action="Index" asp-route-projectId="@Model.ProjectID">Open writer workspace</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@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($$"""
|
||||
<article class="story-review-centre-row">
|
||||
<div>
|
||||
<h2>{{System.Net.WebUtility.HtmlEncode(title)}}</h2>
|
||||
<p>{{System.Net.WebUtility.HtmlEncode(description)}}</p>
|
||||
</div>
|
||||
<strong class="{{statusClass}}">{{System.Net.WebUtility.HtmlEncode(statusText)}}</strong>
|
||||
<a class="btn btn-outline-primary btn-sm{{(blocked ? " disabled" : string.Empty)}}" href="{{href}}"{{disabled}}>{{System.Net.WebUtility.HtmlEncode(actionLabel)}}</a>
|
||||
<span class="story-review-centre-row__count">{{System.Net.WebUtility.HtmlEncode(countText)}}</span>
|
||||
</article>
|
||||
""");
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
<section class="story-pipeline-header" aria-label="Story Intelligence pipeline">
|
||||
<section class="story-pipeline-header" aria-label="Story Intelligence progress">
|
||||
<div class="story-pipeline-current">
|
||||
<p class="eyebrow">Phase @activePhase</p>
|
||||
<h2>@CurrentPhaseTitle(activePhase)</h2>
|
||||
@if (isReviewPhase)
|
||||
{
|
||||
<p>Stage @currentStageNumber of @implementedReviewStages.Count</p>
|
||||
}
|
||||
else if (isComplete)
|
||||
{
|
||||
<p>Final import and completion</p>
|
||||
}
|
||||
else if (isAnalyse)
|
||||
{
|
||||
<p>Story Intelligence Analysis</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Prepare the manuscript for analysis</p>
|
||||
}
|
||||
<p class="eyebrow">Phase @phase</p>
|
||||
<h2>@CurrentPhaseTitle()</h2>
|
||||
<p>@CurrentPhaseDescription()</p>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-progress story-pipeline-progress" aria-label="@CurrentPhaseTitle(activePhase) progress">
|
||||
<span>@ProgressLabel(activePhase, Model.CurrentStage)</span>
|
||||
<div class="onboarding-progress story-pipeline-progress" aria-label="@CurrentPhaseTitle() progress">
|
||||
<span>@ProgressLabel()</span>
|
||||
<div class="onboarding-progress-track">
|
||||
<span style="width:@progressPercent%"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ol class="story-pipeline-phase-list">
|
||||
<li class="@(activePhase > 1 ? "is-complete" : activePhase == 1 ? "is-current" : "is-upcoming")">
|
||||
<strong>Prepare Manuscript</strong>
|
||||
<span>7 completed steps</span>
|
||||
<ul>
|
||||
<li>Welcome</li>
|
||||
<li>Writing Preferences</li>
|
||||
<li>Project</li>
|
||||
<li>Book</li>
|
||||
<li>Connect Word</li>
|
||||
<li>Scan Manuscript</li>
|
||||
<li>Review Chapters</li>
|
||||
</ul>
|
||||
<li class="is-complete">
|
||||
<strong>Core Import</strong>
|
||||
<span>Book structure created from Word</span>
|
||||
</li>
|
||||
<li class="@(activePhase > 2 ? "is-complete" : activePhase == 2 ? "is-current" : "is-upcoming")">
|
||||
<li class="@(isAnalyse || isSceneImport ? "is-current" : "is-complete")">
|
||||
<strong>Analyse Manuscript</strong>
|
||||
<span>Story Intelligence Analysis</span>
|
||||
<span>Scene intelligence saved to PlotDirector</span>
|
||||
</li>
|
||||
<li class="@(activePhase > 3 ? "is-complete" : activePhase == 3 ? "is-current" : "is-upcoming")">
|
||||
<strong>Review Story Intelligence</strong>
|
||||
<span>@ReviewSummary(isReviewPhase, currentStageNumber, implementedReviewStages.Count)</span>
|
||||
<ul class="story-pipeline-review-list">
|
||||
@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";
|
||||
<li class="@stageClass">
|
||||
<span aria-hidden="true">@StageMarker(stageClass, stage.IsImplemented)</span>
|
||||
<strong>@stage.Label</strong>
|
||||
@if (!stage.IsImplemented)
|
||||
{
|
||||
<em>Coming Soon</em>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="@(activePhase == 4 ? "is-current" : "is-upcoming")">
|
||||
<strong>Build Story Database</strong>
|
||||
<span>Final import and completion</span>
|
||||
<li class="@(!isAnalyse && !isSceneImport ? "is-current" : "is-upcoming")">
|
||||
<strong>Review Centre</strong>
|
||||
<span>Independent optional review categories</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
@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
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,60 +1,44 @@
|
||||
@model StoryIntelligencePipelineDashboardViewModel
|
||||
|
||||
<section class="story-review-summary story-pipeline-summary" aria-label="Story Intelligence progress summary">
|
||||
<section class="story-review-summary story-pipeline-summary" aria-label="Story Intelligence review summary">
|
||||
<div>
|
||||
<span>Chapters analysed</span>
|
||||
<strong>✓ @Model.ChaptersAnalysed.ToString("N0")</strong>
|
||||
<strong>@Model.ChaptersAnalysed.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scenes imported</span>
|
||||
<strong>✓ @Model.ScenesImported.ToString("N0")</strong>
|
||||
<strong>@Model.ScenesImported.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Characters identified</span>
|
||||
<strong>@Model.CharactersIdentified.ToString("N0")</strong>
|
||||
<span>Characters</span>
|
||||
<strong>@(Model.CharacterStageComplete ? "Resolved" : "Needs attention")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Characters created / linked</span>
|
||||
<strong>@(Model.CharacterStageComplete ? $"Done: {Model.CharactersCreatedOrLinked:N0}" : "In review")</strong>
|
||||
<span>Locations</span>
|
||||
<strong>@StageStatus(Model.LocationStageComplete)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Locations identified</span>
|
||||
<strong>@Model.LocationsIdentified.ToString("N0")</strong>
|
||||
<span>Assets</span>
|
||||
<strong>@StageStatus(Model.AssetStageComplete)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Locations created / linked</span>
|
||||
<strong>@(Model.LocationStageComplete ? $"Done: {Model.LocationsCreatedOrLinked:N0}" : "In review")</strong>
|
||||
<span>Relationships</span>
|
||||
<strong>@StageStatus(Model.RelationshipStageComplete)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Assets identified</span>
|
||||
<strong>@Model.AssetsIdentified.ToString("N0")</strong>
|
||||
<span>Knowledge</span>
|
||||
<strong>@StageStatus(Model.KnowledgeStageComplete)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Assets created / linked</span>
|
||||
<strong>@(Model.AssetStageComplete ? $"Done: {Model.AssetsCreatedOrLinked:N0}" : "In review")</strong>
|
||||
<span>Plot Lines & Threads</span>
|
||||
<strong>@StageStatus(Model.NarrativeStageComplete)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Relationships identified</span>
|
||||
<strong>@Model.RelationshipsIdentified.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Relationships created / linked</span>
|
||||
<strong>@(Model.RelationshipStageComplete ? $"Done: {Model.RelationshipsCreatedOrLinked:N0}" : "In review")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Knowledge identified</span>
|
||||
<strong>@Model.KnowledgeIdentified.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Knowledge created / linked</span>
|
||||
<strong>@(Model.KnowledgeStageComplete ? $"Done: {Model.KnowledgeCreatedOrLinked:N0}" : "In review")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Plot Lines / Threads found</span>
|
||||
<strong>@Model.NarrativeCandidatesIdentified.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Plot Lines / Threads applied</span>
|
||||
<strong>@(Model.NarrativeStageComplete ? $"Done: {Model.NarrativeCandidatesApplied:N0}" : "In review")</strong>
|
||||
<span>Scene metrics</span>
|
||||
<strong>Available</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@functions {
|
||||
private static string StageStatus(bool complete) => complete ? "Reviewed" : "Optional";
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
19
PlotLine/wwwroot/js/story-review-actions.js
Normal file
19
PlotLine/wwwroot/js/story-review-actions.js
Normal file
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
Loading…
x
Reference in New Issue
Block a user