@model StoryIntelligenceProgressViewModel @{ ViewData["Title"] = "Story Intelligence Review Centre"; var runIds = string.Join(",", Model.Chapters.Select(chapter => chapter.RunID)); var progressPercent = ProgressPercent(Model); }

Story Intelligence

Review Centre

@(Model.CharacterGateSatisfied ? "Your manuscript analysis is complete. Choose any category to inspect or review." : "Resolve Characters first. Once character identities are confirmed, the remaining Story Intelligence categories can be reviewed in any order.")

@if (TempData["OnboardingStoryIntelligenceError"] is string error) {
@error
} @if (TempData["OnboardingStoryIntelligenceMessage"] is string message) {
@message
}
@* "Characters" "Review Characters" "StoryIntelligenceCharacters" "View Characters" new { projectId = Model.ProjectID } *@ @ReviewCharacters() @ReviewModule( "Assets", Model.AssetNeedsAttentionCount, Model.PipelineDashboard.AssetStageComplete, !Model.CharacterGateSatisfied, "Important objects and possessions found in the manuscript.", "Review Assets", "StoryIntelligenceAssets", "View Assets", "StoryAssets", new { projectId = Model.ProjectID }) @ReviewModule( "Knowledge", Model.KnowledgeNeedsAttentionCount, Model.PipelineDashboard.KnowledgeStageComplete, !Model.CharacterGateSatisfied, "Facts, discoveries, beliefs, and suspicions found in the manuscript.", "Review Knowledge", "StoryIntelligenceKnowledge", "View Knowledge", "StoryBible", new { projectId = Model.ProjectID }) @ReviewModule( "Locations", Model.LocationNeedsAttentionCount, Model.PipelineDashboard.LocationStageComplete, !Model.CharacterGateSatisfied, "Places found in the manuscript.", "Review Locations", "StoryIntelligenceLocations", "View Locations", "Locations", new { projectId = Model.ProjectID }) @ReviewPlotSynthesis() @ReviewModule( "Relationships", Model.RelationshipNeedsAttentionCount, Model.PipelineDashboard.RelationshipStageComplete, !Model.CharacterGateSatisfied, "Character relationships found in scene evidence.", "Review Relationships", "StoryIntelligenceRelationships", "View Relationships", "RelationshipMap", new { projectId = Model.ProjectID, bookId = Model.BookID }) @ReviewMetrics(!Model.CharacterGateSatisfied) @if (Model.AnalysisIssueCount > 0) { @ReviewModule( "Analysis Issues", Model.AnalysisIssueCount, false, false, "Some chapters or scenes reported analysis issues.", "Return to Book", "StoryIntelligenceReviewCentre", "Return to Book", "Books", new { id = Model.BookID }) }
Return to Book Open Writer Workspace
@functions { private Microsoft.AspNetCore.Html.IHtmlContent ReviewPlotSynthesis() { var blocked = !Model.CharacterGateSatisfied; var status = "Preparing analysis"; var statusClass = "story-review-centre-tile__status story-review-centre-tile__status--locked"; var description = "Whole-book plot synthesis starts after Characters are resolved."; var actionHtml = "Locked"; if (!blocked) { statusClass = "story-review-centre-tile__status story-review-centre-tile__status--ready"; if (Model.PlotSynthesis.IsQueued) { status = "Preparing"; description = "PlotDirector is preparing the whole-book plot synthesis."; actionHtml = "Preparing"; } else if (Model.PlotSynthesis.IsRunning) { status = "Analysing"; description = "PlotDirector is reading the full book to find sustained Plot Lines, Threads, and scene events."; actionHtml = "Analysing"; } else if (Model.PlotSynthesis.IsFailed) { status = "Failed"; statusClass = "story-review-centre-tile__status story-review-centre-tile__status--warning"; description = string.IsNullOrWhiteSpace(Model.PlotSynthesis.ErrorMessage) ? "Whole-book plot synthesis failed and can be retried." : Model.PlotSynthesis.ErrorMessage; actionHtml = $$"""
"""; } else if (Model.PlotSynthesis.IsCompleted && Model.NarrativeNeedsAttentionCount.GetValueOrDefault() > 0) { status = $"{Model.NarrativeNeedsAttentionCount:N0} need attention"; statusClass = "story-review-centre-tile__status story-review-centre-tile__status--warning"; description = $"{Model.PlotSynthesis.ThreadCount:N0} thread candidates and {Model.PlotSynthesis.EventCount:N0} proposed scene events are ready."; var href = Url.Action("StoryIntelligenceNarrative", "Onboarding", new { batchId = Model.BatchID }) ?? "#"; actionHtml = $"Review Plot Lines & Threads"; } else if (Model.PipelineDashboard.NarrativeStageComplete) { status = "Complete"; description = "Plot Lines and Threads review has been saved."; var href = Url.Action("Index", "Timeline", new { projectId = Model.ProjectID, bookId = Model.BookID }) ?? "#"; actionHtml = $"View on Timeline"; } else { description = "Plot synthesis will start automatically once character resolution is saved."; actionHtml = "Preparing"; } } return new Microsoft.AspNetCore.Html.HtmlString($$"""

Plot Lines & Threads

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

{{System.Net.WebUtility.HtmlEncode(status)}} {{ProgressHtml(Model.PlotSynthesis.IsRunning || Model.PlotSynthesis.IsQueued, null, Model.PlotSynthesis.IsQueued ? "Preparing manuscript..." : "Analysing plot structure...", null)}} {{actionHtml}}
"""); } private Microsoft.AspNetCore.Html.IHtmlContent ReviewCharacters() { var reviewComplete = Model.CharacterNeedsAttentionCount.GetValueOrDefault() == 0 && Model.PipelineDashboard.CharacterStageComplete; var enrichment = Model.CharacterEnrichment; var status = "Ready for review"; var statusClass = "story-review-centre-tile__status story-review-centre-tile__status--warning"; var description = "Resolve character identities before reviewing dependent Story Intelligence."; var actionHtml = $"Review Characters"; var progressHtml = string.Empty; if (reviewComplete && enrichment.IsActive) { status = "Analysing"; statusClass = "story-review-centre-tile__status story-review-centre-tile__status--ready"; description = "PlotDirector is creating character summaries and extracting supported details."; actionHtml = "Analysing"; progressHtml = ProgressHtml(true, enrichment.ProgressPercent, string.IsNullOrWhiteSpace(enrichment.CurrentMessage) ? "Creating summaries and extracting character details..." : enrichment.CurrentMessage, enrichment.TotalCharacters > 0 ? $"{enrichment.ProcessedCharacters:N0} / {enrichment.TotalCharacters:N0}" : null); } else if (reviewComplete && enrichment.IsFailed) { status = "Failed"; statusClass = "story-review-centre-tile__status story-review-centre-tile__status--warning"; description = string.IsNullOrWhiteSpace(enrichment.ErrorMessage) ? "Character enrichment failed and can be retried." : enrichment.ErrorMessage; actionHtml = $$"""
"""; } else if (reviewComplete && enrichment.IsCompleted) { status = "Complete"; statusClass = "story-review-centre-tile__status story-review-centre-tile__status--ready"; description = "Character identities, summaries, and supported details are ready."; var href = Url.Action("Index", "Characters", new { projectId = Model.ProjectID }) ?? "#"; actionHtml = $"View Characters"; } else if (reviewComplete) { status = "Analysing"; statusClass = "story-review-centre-tile__status story-review-centre-tile__status--ready"; description = "Character enrichment is queued."; actionHtml = "Preparing"; progressHtml = ProgressHtml(true, null, "Preparing character analysis...", null); } return new Microsoft.AspNetCore.Html.HtmlString($$"""

Characters

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

{{System.Net.WebUtility.HtmlEncode(status)}}
{{progressHtml}}
{{actionHtml}}
"""); } private string StatusText(string title, int? count, bool complete, bool blocked) { if (blocked) { return "Locked until Characters are resolved"; } if (string.Equals(title, "Scene Metrics", StringComparison.Ordinal)) { return Model.MetricValueCount > 0 ? "Available" : "No metrics available"; } if (complete) { return "Complete"; } if (!count.HasValue) { return "Ready"; } return count switch { 0 => "No review needed", 1 => "1 needs attention", _ => $"{count:N0} need attention" }; } private Microsoft.AspNetCore.Html.IHtmlContent ReviewMetrics(bool blocked) { var hasMetrics = Model.MetricValueCount > 0; var status = StatusText("Scene Metrics", Model.MetricValueCount, false, blocked); var statusClass = blocked ? "story-review-centre-tile__status story-review-centre-tile__status--locked" : hasMetrics ? "story-review-centre-tile__status story-review-centre-tile__status--ready" : "story-review-centre-tile__status story-review-centre-tile__status--locked"; var href = Url.Action("Index", "Timeline", new { projectId = Model.ProjectID, bookId = Model.BookID }) ?? "#"; var actionHtml = blocked ? "Locked" : hasMetrics ? $"View on Timeline" : "No action"; return new Microsoft.AspNetCore.Html.HtmlString($$"""

Scene Metrics

Auto-applied metrics from analysed scenes.

{{System.Net.WebUtility.HtmlEncode(status)}} {{actionHtml}}
"""); } private Microsoft.AspNetCore.Html.IHtmlContent ReviewModule( string title, int? count, bool complete, bool blocked, string description, string reviewActionLabel, string reviewAction, string completeActionLabel, string completeController, object completeRouteValues, bool primary = false) { var status = StatusText(title, count, complete, blocked); var statusClass = blocked ? "story-review-centre-tile__status story-review-centre-tile__status--locked" : (count.GetValueOrDefault() > 0 || (!complete && !count.HasValue)) ? "story-review-centre-tile__status story-review-centre-tile__status--warning" : "story-review-centre-tile__status story-review-centre-tile__status--ready"; var tileClass = "story-review-centre-tile" + (primary ? " story-review-centre-tile--primary" : string.Empty) + (blocked ? " story-review-centre-tile--locked" : string.Empty); var href = complete ? Url.Action("Index", completeController, completeRouteValues) ?? "#" : Url.Action(reviewAction, "Onboarding", new { batchId = Model.BatchID }) ?? "#"; var actionLabel = complete ? completeActionLabel : reviewActionLabel; var actionHtml = blocked ? "Locked" : $"{System.Net.WebUtility.HtmlEncode(actionLabel)}"; return new Microsoft.AspNetCore.Html.HtmlString($$"""

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

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

{{System.Net.WebUtility.HtmlEncode(status)}} {{actionHtml}}
"""); } private static string ProgressHtml(bool visible, int? percent, string message, string? countText) { if (!visible) { return string.Empty; } var determinate = percent.HasValue; var valueNow = percent.GetValueOrDefault(); return $$"""

{{System.Net.WebUtility.HtmlEncode(message)}}{{(string.IsNullOrWhiteSpace(countText) ? string.Empty : $" {System.Net.WebUtility.HtmlEncode(countText)}")}}

"""; } private static int ProgressPercent(StoryIntelligenceProgressViewModel model) { if (model.ChapterCount == 0) { return 0; } var chapterProgress = model.Chapters.Sum(ChapterProgress); return Math.Clamp((int)Math.Floor(chapterProgress * 100m / model.ChapterCount), 0, 100); } private static decimal ChapterProgress(StoryIntelligenceOnboardingChapterViewModel chapter) { if (chapter.HasCompletedCommit || chapter.IsRunComplete || chapter.IsFailed) { return 1m; } if (chapter.Status == StoryIntelligenceRunStatuses.Pending) { return 0m; } if ((chapter.TotalDetectedScenes ?? 0) > 0) { var sceneProgress = Math.Clamp((chapter.CompletedScenes ?? 0) / Math.Max(1m, chapter.TotalDetectedScenes ?? 1), 0m, 1m); return Math.Clamp(0.25m + (sceneProgress * 0.7m), 0m, 0.95m); } return 0.15m; } } @section Scripts { }