@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 })
Locations
@Model.LocationReview.TotalCandidateCount locations identified
@Model.LocationReview.SignificantCount significant ยท @Model.LocationReview.UncertainCount need review
Explore story settings. Review uncertain identities when your judgement is needed.
@if (Model.CharacterGateSatisfied) {
Explore Locations
@if (Model.PipelineDashboard.LocationStageComplete) { View Locations }
} else { Locked }
@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 })
}
@foreach (var chapter in Model.Chapters.OrderBy(chapter => chapter.ChapterNumber))
{
@chapter.Status
@((chapter.CompletedScenes ?? 0).ToString("N0"))
@((chapter.TotalDetectedScenes ?? 0).ToString("N0"))
@((chapter.FailedScenes ?? 0).ToString("N0"))
@(chapter.CurrentMessage ?? string.Empty)
@(chapter.ErrorMessage ?? string.Empty)
@(chapter.CurrentSceneDisplayTitle ?? string.Empty)
@(chapter.LatestSceneSummary ?? string.Empty)
}
@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 {
}