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