427 lines
22 KiB
Plaintext
427 lines
22 KiB
Plaintext
@model StoryIntelligenceProgressViewModel
|
|
@{
|
|
ViewData["Title"] = "Story Intelligence Review Centre";
|
|
var runIds = string.Join(",", Model.Chapters.Select(chapter => chapter.RunID));
|
|
var progressPercent = ProgressPercent(Model);
|
|
}
|
|
|
|
<section class="story-review-centre-shell"
|
|
aria-labelledby="story-review-centre-title"
|
|
data-story-intelligence-onboarding
|
|
data-story-intelligence-batch-id="@Model.BatchID"
|
|
data-story-intelligence-run-ids="@runIds"
|
|
data-story-max-progress-percent="@progressPercent"
|
|
data-story-intelligence-has-active-runs="@Model.HasActiveRuns.ToString().ToLowerInvariant()">
|
|
<div class="story-review-centre-workspace">
|
|
<div class="story-review-centre-heading">
|
|
<p class="eyebrow">Story Intelligence</p>
|
|
<h1 id="story-review-centre-title">Review Centre</h1>
|
|
<p>@(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.")</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="story-review-centre-grid" aria-label="Story Intelligence review categories">
|
|
@* "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 })
|
|
}
|
|
</section>
|
|
|
|
<div class="story-run-state" aria-hidden="true" hidden>
|
|
@foreach (var chapter in Model.Chapters.OrderBy(chapter => chapter.ChapterNumber))
|
|
{
|
|
<div data-story-run-id="@chapter.RunID"
|
|
data-story-chapter-title="@chapter.ChapterTitle"
|
|
data-story-raw-status="@chapter.Status"
|
|
data-story-chapter-number="@chapter.ChapterNumber"
|
|
data-story-current-stage="@(chapter.CurrentStage ?? string.Empty)"
|
|
data-story-current-scene-id="@(chapter.CurrentSceneID?.ToString() ?? string.Empty)"
|
|
data-story-current-scene-number="@(chapter.CurrentSceneNumber?.ToString() ?? string.Empty)"
|
|
data-story-current-scene-title="@(chapter.CurrentSceneDisplayTitle ?? string.Empty)"
|
|
data-story-updated-utc="@(chapter.UpdatedUtc?.ToUniversalTime().ToString("O") ?? string.Empty)"
|
|
data-story-source-word-count="@(chapter.SourceWordCount ?? 0)"
|
|
data-story-duration-ms="@(chapter.TotalDurationMs ?? 0)">
|
|
<span data-story-run-status>@chapter.Status</span>
|
|
<span data-story-run-completed>@((chapter.CompletedScenes ?? 0).ToString("N0"))</span>
|
|
<span data-story-run-total>@((chapter.TotalDetectedScenes ?? 0).ToString("N0"))</span>
|
|
<span data-story-run-failed>@((chapter.FailedScenes ?? 0).ToString("N0"))</span>
|
|
<span data-story-run-message>@(chapter.CurrentMessage ?? string.Empty)</span>
|
|
<span data-story-run-error>@(chapter.ErrorMessage ?? string.Empty)</span>
|
|
<span data-story-run-current-scene-title>@(chapter.CurrentSceneDisplayTitle ?? string.Empty)</span>
|
|
<span data-story-run-latest-summary>@(chapter.LatestSceneSummary ?? string.Empty)</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="onboarding-actions story-review-centre-actions">
|
|
<a class="btn btn-outline-secondary" asp-controller="Books" asp-action="Details" asp-route-id="@Model.BookID">Return to Book</a>
|
|
<a class="btn btn-outline-primary" asp-controller="Writer" asp-action="Index" asp-route-projectId="@Model.ProjectID">Open Writer Workspace</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
@functions {
|
|
private 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 = "<span class=\"btn btn-outline-secondary btn-sm disabled\" aria-disabled=\"true\">Locked</span>";
|
|
|
|
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 = "<span class=\"btn btn-outline-primary btn-sm disabled\" aria-disabled=\"true\">Preparing</span>";
|
|
}
|
|
else if (Model.PlotSynthesis.IsRunning)
|
|
{
|
|
status = "Analysing";
|
|
description = "PlotDirector is reading the full book to find sustained Plot Lines, Threads, and scene events.";
|
|
actionHtml = "<span class=\"btn btn-outline-primary btn-sm disabled\" aria-disabled=\"true\">Analysing</span>";
|
|
}
|
|
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 = $$"""
|
|
<form method="post" action="{{Url.Action("RetryStoryIntelligencePlotSynthesis", "Onboarding")}}">
|
|
<input type="hidden" name="batchId" value="{{Model.BatchID}}" />
|
|
<button class="btn btn-outline-primary btn-sm" type="submit">Retry</button>
|
|
</form>
|
|
""";
|
|
}
|
|
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 = $"<a class=\"btn btn-outline-primary btn-sm\" href=\"{System.Net.WebUtility.HtmlEncode(href)}\">Review Plot Lines & Threads</a>";
|
|
}
|
|
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 = $"<a class=\"btn btn-outline-primary btn-sm\" href=\"{System.Net.WebUtility.HtmlEncode(href)}\">View on Timeline</a>";
|
|
}
|
|
else
|
|
{
|
|
description = "Plot synthesis will start automatically once character resolution is saved.";
|
|
actionHtml = "<span class=\"btn btn-outline-primary btn-sm disabled\" aria-disabled=\"true\">Preparing</span>";
|
|
}
|
|
}
|
|
|
|
return new Microsoft.AspNetCore.Html.HtmlString($$"""
|
|
<article class="story-review-centre-tile{{(blocked ? " story-review-centre-tile--locked" : string.Empty)}}" data-plot-synthesis-tile data-book-id="{{Model.BookID}}">
|
|
<div>
|
|
<h2>Plot Lines & Threads</h2>
|
|
<p>{{System.Net.WebUtility.HtmlEncode(description)}}</p>
|
|
</div>
|
|
<strong class="{{statusClass}}">{{System.Net.WebUtility.HtmlEncode(status)}}</strong>
|
|
{{ProgressHtml(Model.PlotSynthesis.IsRunning || Model.PlotSynthesis.IsQueued, null, Model.PlotSynthesis.IsQueued ? "Preparing manuscript..." : "Analysing plot structure...", null)}}
|
|
{{actionHtml}}
|
|
</article>
|
|
""");
|
|
}
|
|
|
|
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 = $"<a class=\"btn btn-outline-primary btn-sm\" href=\"{System.Net.WebUtility.HtmlEncode(Url.Action("StoryIntelligenceCharacters", "Onboarding", new { batchId = Model.BatchID }) ?? "#")}\">Review Characters</a>";
|
|
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 = "<span class=\"btn btn-outline-primary btn-sm disabled\" aria-disabled=\"true\">Analysing</span>";
|
|
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 = $$"""
|
|
<form method="post" action="{{Url.Action("RetryStoryIntelligenceCharacterEnrichment", "Onboarding")}}">
|
|
<input type="hidden" name="batchId" value="{{Model.BatchID}}" />
|
|
<button class="btn btn-outline-primary btn-sm" type="submit">Retry</button>
|
|
</form>
|
|
""";
|
|
}
|
|
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 = $"<a class=\"btn btn-outline-primary btn-sm\" href=\"{System.Net.WebUtility.HtmlEncode(href)}\">View Characters</a>";
|
|
}
|
|
else if (reviewComplete)
|
|
{
|
|
status = "Analysing";
|
|
statusClass = "story-review-centre-tile__status story-review-centre-tile__status--ready";
|
|
description = "Character enrichment is queued.";
|
|
actionHtml = "<span class=\"btn btn-outline-primary btn-sm disabled\" aria-disabled=\"true\">Preparing</span>";
|
|
progressHtml = ProgressHtml(true, null, "Preparing character analysis...", null);
|
|
}
|
|
|
|
return new Microsoft.AspNetCore.Html.HtmlString($$"""
|
|
<article class="story-review-centre-tile story-review-centre-tile--primary" data-character-enrichment-tile data-book-id="{{Model.BookID}}" data-project-id="{{Model.ProjectID}}">
|
|
<div>
|
|
<h2>Characters</h2>
|
|
<p data-character-enrichment-description>{{System.Net.WebUtility.HtmlEncode(description)}}</p>
|
|
</div>
|
|
<strong class="{{statusClass}}" data-character-enrichment-status>{{System.Net.WebUtility.HtmlEncode(status)}}</strong>
|
|
<div data-character-enrichment-progress>{{progressHtml}}</div>
|
|
<div data-character-enrichment-action>{{actionHtml}}</div>
|
|
</article>
|
|
""");
|
|
}
|
|
|
|
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
|
|
? "<span class=\"btn btn-outline-secondary btn-sm disabled\" aria-disabled=\"true\">Locked</span>"
|
|
: hasMetrics
|
|
? $"<a class=\"btn btn-outline-primary btn-sm\" href=\"{System.Net.WebUtility.HtmlEncode(href)}\">View on Timeline</a>"
|
|
: "<span class=\"btn btn-outline-secondary btn-sm disabled\" aria-disabled=\"true\">No action</span>";
|
|
|
|
return new Microsoft.AspNetCore.Html.HtmlString($$"""
|
|
<article class="story-review-centre-tile{{(blocked ? " story-review-centre-tile--locked" : string.Empty)}}">
|
|
<div>
|
|
<h2>Scene Metrics</h2>
|
|
<p>Auto-applied metrics from analysed scenes.</p>
|
|
</div>
|
|
<strong class="{{statusClass}}">{{System.Net.WebUtility.HtmlEncode(status)}}</strong>
|
|
{{actionHtml}}
|
|
</article>
|
|
""");
|
|
}
|
|
|
|
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
|
|
? "<span class=\"btn btn-outline-secondary btn-sm disabled\" aria-disabled=\"true\">Locked</span>"
|
|
: $"<a class=\"btn btn-outline-primary btn-sm\" href=\"{System.Net.WebUtility.HtmlEncode(href)}\">{System.Net.WebUtility.HtmlEncode(actionLabel)}</a>";
|
|
|
|
return new Microsoft.AspNetCore.Html.HtmlString($$"""
|
|
<article class="{{tileClass}}">
|
|
<div>
|
|
<h2>{{System.Net.WebUtility.HtmlEncode(title)}}</h2>
|
|
<p>{{System.Net.WebUtility.HtmlEncode(description)}}</p>
|
|
</div>
|
|
<strong class="{{statusClass}}">{{System.Net.WebUtility.HtmlEncode(status)}}</strong>
|
|
{{actionHtml}}
|
|
</article>
|
|
""");
|
|
}
|
|
|
|
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 $$"""
|
|
<div class="story-review-centre-progress" data-progress-kind="{{(determinate ? "determinate" : "indeterminate")}}">
|
|
<div class="story-review-centre-progress__bar" role="progressbar" aria-valuemin="0" aria-valuemax="100"{{(determinate ? $" aria-valuenow=\"{valueNow}\"" : string.Empty)}}>
|
|
<span style="width: {{(determinate ? valueNow : 100)}}%"></span>
|
|
</div>
|
|
<p>{{System.Net.WebUtility.HtmlEncode(message)}}{{(string.IsNullOrWhiteSpace(countText) ? string.Empty : $" {System.Net.WebUtility.HtmlEncode(countText)}")}}</p>
|
|
</div>
|
|
""";
|
|
}
|
|
|
|
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 {
|
|
<script src="~/js/story-intelligence-progress.js" asp-append-version="true"></script>
|
|
<script src="~/js/story-review-centre-progress.js" asp-append-version="true"></script>
|
|
}
|