Phase 20AD – Split Story Intelligence Progress and Review Screens
This commit is contained in:
parent
908660db1c
commit
124d380b0d
@ -84,6 +84,20 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/review")]
|
||||
public async Task<IActionResult> StoryIntelligenceReview(Guid batchId)
|
||||
{
|
||||
var model = await storyIntelligence.GetProgressAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return model.HasActiveRuns
|
||||
? RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId })
|
||||
: View(model);
|
||||
}
|
||||
|
||||
[HttpPost("story-intelligence/cancel")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CancelStoryIntelligence(Guid batchId)
|
||||
@ -95,7 +109,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
||||
}
|
||||
|
||||
TempData["OnboardingStoryIntelligenceMessage"] = "Analysis cancellation requested.";
|
||||
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
||||
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
||||
}
|
||||
|
||||
[HttpPost("story-intelligence/commit")]
|
||||
@ -115,7 +129,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
||||
return RedirectToAction("Index", "Writer", new { projectId = progress.ProjectID });
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
||||
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/complete")]
|
||||
|
||||
@ -563,19 +563,19 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Take(3)
|
||||
.Select(name => $"Possible character detected: {name}"));
|
||||
.Select(name => $"Possible character found: {name}"));
|
||||
discoveries.AddRange((scene.Locations ?? [])
|
||||
.Select(location => TrimOptional(location.Name, 120))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Take(3)
|
||||
.Select(name => $"Possible location detected: {name}"));
|
||||
.Select(name => $"Possible location found: {name}"));
|
||||
discoveries.AddRange((scene.Assets ?? [])
|
||||
.Select(asset => TrimOptional(asset.Name, 120))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Take(2)
|
||||
.Select(name => $"Possible asset detected: {name}"));
|
||||
.Select(name => $"Possible asset found: {name}"));
|
||||
discoveries.AddRange((scene.Relationships ?? [])
|
||||
.Select(relationship => BuildRelationshipDiscovery(relationship))
|
||||
.Where(text => !string.IsNullOrWhiteSpace(text))
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
<h2>Analysis has already started</h2>
|
||||
<p>You can continue reviewing progress for the approved chapters.</p>
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceProgress" asp-route-batchId="@Model.ExistingBatchID">Review scenes</a>
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.ExistingBatchID">Review analysis</a>
|
||||
<form asp-action="ScanManuscript" method="post">
|
||||
<button class="btn btn-outline-secondary" type="submit">Scan manuscript again</button>
|
||||
</form>
|
||||
|
||||
@ -1,27 +1,22 @@
|
||||
@model StoryIntelligenceProgressViewModel
|
||||
@using System.Security.Claims
|
||||
@inject IConfiguration Configuration
|
||||
@{
|
||||
ViewData["Title"] = "Review scenes";
|
||||
ViewData["Title"] = "Reading your manuscript";
|
||||
var runIds = string.Join(",", Model.Chapters.Select(chapter => chapter.RunID));
|
||||
var progressPercent = ProgressPercent(Model);
|
||||
var userEmail = User.FindFirstValue(ClaimTypes.Email);
|
||||
var adminEmails = Configuration.GetSection("Admin:AllowedEmails").Get<string[]>() ?? [];
|
||||
var isAdmin = !string.IsNullOrWhiteSpace(userEmail)
|
||||
&& adminEmails.Any(email => string.Equals(email, userEmail, StringComparison.OrdinalIgnoreCase));
|
||||
var currentChapter = CurrentChapter(Model);
|
||||
}
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="story-progress-title">
|
||||
<div class="onboarding-panel onboarding-review-panel"
|
||||
<div class="onboarding-panel onboarding-review-panel story-live-progress"
|
||||
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="onboarding-copy">
|
||||
<div class="onboarding-copy story-live-hero">
|
||||
<p class="eyebrow">@Model.BookTitle</p>
|
||||
<h1 id="story-progress-title">Review scenes</h1>
|
||||
<p>PlotDirector is reading the approved chapters and preparing scene suggestions for you to review.</p>
|
||||
<h1 id="story-progress-title">Reading your manuscript...</h1>
|
||||
<p>PlotDirector is analysing your chapters and preparing scenes for review. You can safely leave this page and return later.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
@ -33,191 +28,95 @@
|
||||
<div class="alert alert-success">@message</div>
|
||||
}
|
||||
|
||||
<section class="alert alert-info" data-story-reassurance>
|
||||
@if (Model.Chapters.Any(chapter => chapter.CanCommit || chapter.HasCompletedCommit))
|
||||
{
|
||||
<span>Scenes can be created now. Characters, locations, assets and relationships remain suggestions for later review.</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Nothing has been added to your project yet. PlotDirector is preparing suggestions. You will review them before anything is created.</span>
|
||||
}
|
||||
<section class="alert alert-info">
|
||||
Nothing has been added to your project yet. PlotDirector is preparing suggestions. You will review them before anything is created.
|
||||
</section>
|
||||
|
||||
<div class="onboarding-scan-counts onboarding-review-counts">
|
||||
<div>
|
||||
<span>Import progress</span>
|
||||
<strong data-story-overall-percent>@progressPercent%</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Chapters read</span>
|
||||
<strong><span data-story-chapters-completed>@Model.CompletedChapterCount</span> of @Model.ChapterCount</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scenes read</span>
|
||||
<strong><span data-story-scenes-completed>@Model.TotalCompletedScenes</span> so far</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Needs review</span>
|
||||
<strong data-story-failures>@(Model.FailedChapterCount + Model.TotalFailedScenes)</strong>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted">These are chapters or scenes PlotDirector could not fully analyse. You can review them after the run completes.</p>
|
||||
|
||||
<div class="progress mb-3" role="progressbar" aria-label="Story Intelligence progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="@progressPercent">
|
||||
<div class="progress-bar" data-story-overall-progress-bar style="width:@progressPercent%"></div>
|
||||
</div>
|
||||
|
||||
<section class="onboarding-review-grid" aria-label="Story Intelligence progress">
|
||||
<div class="onboarding-review-section">
|
||||
<h2>Latest from your manuscript</h2>
|
||||
<div class="onboarding-scan-counts onboarding-review-counts">
|
||||
<div>
|
||||
<span>Current chapter</span>
|
||||
<strong data-story-current-chapter>@(CurrentChapter(Model)?.ChapterTitle ?? "Waiting")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Current scene</span>
|
||||
<strong data-story-current-scene>@CurrentScene(Model)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Current stage</span>
|
||||
<strong data-story-current-stage>@FriendlyStage(CurrentChapter(Model)?.CurrentStage, CurrentChapter(Model)?.Status)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Elapsed</span>
|
||||
<strong data-story-elapsed>@Elapsed(Model)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Estimated remaining</span>
|
||||
<strong data-story-remaining>@EstimateRemaining(Model)</strong>
|
||||
</div>
|
||||
<section class="story-live-progress-card" aria-label="Import progress">
|
||||
<div class="story-live-progress-heading">
|
||||
<div>
|
||||
<p class="eyebrow mb-1">Import progress</p>
|
||||
<strong data-story-overall-percent>@progressPercent%</strong>
|
||||
</div>
|
||||
<p data-story-current-message>@FriendlyMessage(CurrentChapter(Model))</p>
|
||||
<p class="lead" data-story-latest-summary hidden></p>
|
||||
<span data-story-current-stage>@FriendlyStage(currentChapter?.CurrentStage, currentChapter?.Status)</span>
|
||||
</div>
|
||||
<div class="progress" role="progressbar" aria-label="Story Intelligence progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="@progressPercent">
|
||||
<div class="progress-bar" data-story-overall-progress-bar style="width:@progressPercent%"></div>
|
||||
</div>
|
||||
<div class="story-live-stats">
|
||||
<div>
|
||||
<span>Chapters read</span>
|
||||
<strong><span data-story-chapters-completed>@Model.CompletedChapterCount</span> of @Model.ChapterCount</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scenes read</span>
|
||||
<strong><span data-story-scenes-completed>@Model.TotalCompletedScenes</span> so far</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Needs review</span>
|
||||
<strong data-story-failures>@(Model.FailedChapterCount + Model.TotalFailedScenes)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Elapsed</span>
|
||||
<strong data-story-elapsed>@Elapsed(Model)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Estimated remaining</span>
|
||||
<strong data-story-remaining>@EstimateRemaining(Model)</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="story-live-current" aria-label="Current activity">
|
||||
<div>
|
||||
<p class="eyebrow">Current chapter</p>
|
||||
<h2 data-story-current-chapter>@(currentChapter?.ChapterTitle ?? "Waiting")</h2>
|
||||
<p data-story-current-message>@FriendlyMessage(currentChapter)</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="eyebrow">Current scene</p>
|
||||
<strong data-story-current-scene>@CurrentScene(Model)</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="story-live-feed-grid" aria-label="Live manuscript feed">
|
||||
<div>
|
||||
<h2>Latest from your manuscript</h2>
|
||||
<p class="lead story-latest-summary" data-story-latest-summary hidden></p>
|
||||
<p class="text-muted" data-story-latest-empty>Scene summaries will appear here as PlotDirector reads.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Recent discoveries</h2>
|
||||
<ul data-story-discovery-feed></ul>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-review-section">
|
||||
<h2>Chapters</h2>
|
||||
<div class="onboarding-review-chapters">
|
||||
@foreach (var chapter in Model.Chapters.OrderBy(chapter => chapter.ChapterNumber))
|
||||
{
|
||||
<details class="onboarding-review-chapter" open data-story-run-id="@chapter.RunID" data-story-chapter-title="@chapter.ChapterTitle" data-story-raw-status="@chapter.Status">
|
||||
<summary>
|
||||
<span>@chapter.ChapterNumber. @chapter.ChapterTitle</span>
|
||||
<strong data-story-chapter-status-label>@ChapterStatusLabel(chapter)</strong>
|
||||
</summary>
|
||||
<div class="onboarding-scan-counts onboarding-review-counts">
|
||||
<div>
|
||||
<span>Status</span>
|
||||
<strong data-story-run-status>@FriendlyStatus(chapter.Status)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scenes detected</span>
|
||||
<strong><span data-story-run-completed>@((chapter.CompletedScenes ?? 0).ToString("N0"))</span> / <span data-story-run-total>@((chapter.TotalDetectedScenes ?? 0).ToString("N0"))</span></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Needs review</span>
|
||||
<strong data-story-run-failed>@((chapter.FailedScenes ?? 0).ToString("N0"))</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p data-story-run-message>@FriendlyMessage(chapter)</p>
|
||||
@if ((chapter.FailedScenes ?? 0) > 0)
|
||||
{
|
||||
<p class="text-muted" data-story-run-review-note>Needs review after analysis completes.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted" data-story-run-review-note hidden></p>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(chapter.ErrorMessage))
|
||||
{
|
||||
<p class="text-danger" data-story-run-error>@chapter.ErrorMessage</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-danger" data-story-run-error hidden></p>
|
||||
}
|
||||
@if (chapter.Warnings.Any())
|
||||
{
|
||||
<ul>
|
||||
@foreach (var warning in chapter.Warnings)
|
||||
{
|
||||
<li>@warning</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
@if (chapter.Blockers.Any())
|
||||
{
|
||||
<ul>
|
||||
@foreach (var blocker in chapter.Blockers)
|
||||
{
|
||||
<li class="text-danger">@blocker</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
<div class="onboarding-actions">
|
||||
@if (chapter.HasCompletedCommit)
|
||||
{
|
||||
<span class="btn btn-outline-secondary disabled">@chapter.ScenesCreated scene(s) created</span>
|
||||
}
|
||||
else if (chapter.CanCommit && Model.BatchID.HasValue)
|
||||
{
|
||||
<form asp-action="CommitStoryIntelligence" method="post">
|
||||
<input type="hidden" name="batchId" value="@Model.BatchID" />
|
||||
<input type="hidden" name="runId" value="@chapter.RunID" />
|
||||
<button class="btn btn-primary" type="submit">Create scenes</button>
|
||||
</form>
|
||||
}
|
||||
else if (chapter.IsRunComplete)
|
||||
{
|
||||
<span class="btn btn-outline-secondary disabled">Review needed</span>
|
||||
}
|
||||
</div>
|
||||
@if (isAdmin)
|
||||
{
|
||||
<details class="mt-3">
|
||||
<summary>Diagnostics</summary>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">Run ID</dt>
|
||||
<dd class="col-sm-8">@chapter.RunID</dd>
|
||||
<dt class="col-sm-4">Raw status</dt>
|
||||
<dd class="col-sm-8" data-story-run-raw-status>@chapter.Status</dd>
|
||||
<dt class="col-sm-4">Raw stage</dt>
|
||||
<dd class="col-sm-8" data-story-run-raw-stage>@(chapter.CurrentStage ?? "-")</dd>
|
||||
<dt class="col-sm-4">Tokens</dt>
|
||||
<dd class="col-sm-8" data-story-run-tokens>@(chapter.TotalTokens?.ToString("N0") ?? "-")</dd>
|
||||
@if (!string.IsNullOrWhiteSpace(chapter.FailureStage))
|
||||
{
|
||||
<dt class="col-sm-4">Failure stage</dt>
|
||||
<dd class="col-sm-8">@chapter.FailureStage</dd>
|
||||
}
|
||||
</dl>
|
||||
</details>
|
||||
}
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<span data-story-run-status>@FriendlyStatus(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>@FriendlyMessage(chapter)</span>
|
||||
<span data-story-run-error>@(chapter.ErrorMessage ?? string.Empty)</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
@if (Model.BatchID.HasValue)
|
||||
{
|
||||
<form asp-action="ScanManuscript" method="post">
|
||||
<button class="btn btn-outline-secondary" type="submit" disabled="@Model.HasActiveRuns">Scan manuscript again</button>
|
||||
</form>
|
||||
<form asp-action="CancelStoryIntelligence" method="post">
|
||||
<input type="hidden" name="batchId" value="@Model.BatchID" />
|
||||
<button class="btn btn-outline-secondary" type="submit" disabled="@(!Model.HasActiveRuns)">Cancel analysis</button>
|
||||
</form>
|
||||
<a class="btn btn-primary @(Model.AllCommitted ? string.Empty : "disabled")"
|
||||
asp-action="StoryIntelligenceComplete"
|
||||
<a class="btn btn-primary @(Model.HasActiveRuns ? "disabled" : string.Empty)"
|
||||
data-story-review-link
|
||||
asp-action="StoryIntelligenceReview"
|
||||
asp-route-batchId="@Model.BatchID"
|
||||
aria-disabled="@(!Model.AllCommitted)">Continue</a>
|
||||
aria-disabled="@Model.HasActiveRuns">Review analysis</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -228,39 +127,14 @@
|
||||
}
|
||||
|
||||
@functions {
|
||||
private static string ChapterStatusLabel(StoryIntelligenceOnboardingChapterViewModel chapter)
|
||||
{
|
||||
if (chapter.HasCompletedCommit)
|
||||
{
|
||||
return "Scenes created";
|
||||
}
|
||||
|
||||
if (chapter.CanCommit)
|
||||
{
|
||||
return "Ready to create";
|
||||
}
|
||||
|
||||
if (chapter.IsActive)
|
||||
{
|
||||
return "Analysing";
|
||||
}
|
||||
|
||||
if (chapter.IsFailed)
|
||||
{
|
||||
return "Needs attention";
|
||||
}
|
||||
|
||||
return "Reviewing";
|
||||
}
|
||||
|
||||
private static string FriendlyStatus(string? status)
|
||||
=> status switch
|
||||
{
|
||||
StoryIntelligenceRunStatuses.Pending => "Waiting",
|
||||
StoryIntelligenceRunStatuses.Running => "Reading",
|
||||
StoryIntelligenceRunStatuses.Completed => "Ready to review",
|
||||
StoryIntelligenceRunStatuses.CompletedWithWarnings => "Ready to review, with notes",
|
||||
StoryIntelligenceRunStatuses.Failed => "Needs attention",
|
||||
StoryIntelligenceRunStatuses.Completed => "Ready to create",
|
||||
StoryIntelligenceRunStatuses.CompletedWithWarnings => "Ready with notes",
|
||||
StoryIntelligenceRunStatuses.Failed => "Needs review",
|
||||
StoryIntelligenceRunStatuses.Cancelled => "Cancelled",
|
||||
_ => "Waiting"
|
||||
};
|
||||
@ -274,7 +148,7 @@
|
||||
|
||||
if (status is StoryIntelligenceRunStatuses.Failed)
|
||||
{
|
||||
return "Needs attention";
|
||||
return "Needs review";
|
||||
}
|
||||
|
||||
if (status is StoryIntelligenceRunStatuses.Cancelled)
|
||||
@ -285,13 +159,11 @@
|
||||
return stage switch
|
||||
{
|
||||
"Queued" => "Waiting",
|
||||
StoryIntelligenceFailureStages.DocumentRead => "Preparing chapter",
|
||||
StoryIntelligenceFailureStages.ChapterStructure => "Finding scenes",
|
||||
StoryIntelligenceFailureStages.SceneSplit => "Preparing scenes",
|
||||
StoryIntelligenceFailureStages.SceneIntelligence => "Reading scenes",
|
||||
StoryIntelligenceFailureStages.Validation => "Checking results",
|
||||
StoryIntelligenceFailureStages.Persistence => "Saving analysis",
|
||||
StoryIntelligenceRunStatuses.Completed => "Ready to review",
|
||||
StoryIntelligenceFailureStages.SceneSplit => "Preparing review",
|
||||
StoryIntelligenceFailureStages.Validation => "Preparing review",
|
||||
StoryIntelligenceFailureStages.Persistence => "Preparing review",
|
||||
_ => "Reading"
|
||||
};
|
||||
}
|
||||
@ -310,13 +182,12 @@
|
||||
|
||||
if (chapter.Status == StoryIntelligenceRunStatuses.Failed)
|
||||
{
|
||||
return "This chapter needs attention before scenes can be created.";
|
||||
return "This chapter needs review before scenes can be created.";
|
||||
}
|
||||
|
||||
return chapter.CurrentStage switch
|
||||
{
|
||||
"Queued" => "Waiting to read this chapter.",
|
||||
StoryIntelligenceFailureStages.DocumentRead => "Preparing this chapter.",
|
||||
StoryIntelligenceFailureStages.ChapterStructure => "Finding scenes in this chapter.",
|
||||
StoryIntelligenceFailureStages.SceneSplit => "Preparing the detected scenes.",
|
||||
StoryIntelligenceFailureStages.SceneIntelligence => "Reading the detected scenes.",
|
||||
@ -345,12 +216,7 @@
|
||||
|
||||
private static decimal ChapterProgress(StoryIntelligenceOnboardingChapterViewModel chapter)
|
||||
{
|
||||
if (chapter.HasCompletedCommit || chapter.IsRunComplete)
|
||||
{
|
||||
return 1m;
|
||||
}
|
||||
|
||||
if (chapter.IsFailed)
|
||||
if (chapter.HasCompletedCommit || chapter.IsRunComplete || chapter.IsFailed)
|
||||
{
|
||||
return 1m;
|
||||
}
|
||||
@ -379,29 +245,7 @@
|
||||
}
|
||||
|
||||
private static string EstimateRemaining(StoryIntelligenceProgressViewModel model)
|
||||
{
|
||||
if (!model.HasActiveRuns)
|
||||
{
|
||||
return "Almost done";
|
||||
}
|
||||
|
||||
if (model.TotalCompletedScenes <= 0)
|
||||
{
|
||||
return "Estimating...";
|
||||
}
|
||||
|
||||
var remainingScenes = Math.Max(0, model.TotalDetectedScenes - model.TotalCompletedScenes);
|
||||
if (remainingScenes == 0)
|
||||
{
|
||||
return "Almost done";
|
||||
}
|
||||
|
||||
var elapsedMs = model.Chapters.Max(chapter => chapter.TotalDurationMs ?? 0);
|
||||
var averageSceneMinutes = Math.Max(1, (int)Math.Ceiling(TimeSpan.FromMilliseconds(elapsedMs).TotalMinutes / Math.Max(1, model.TotalCompletedScenes)));
|
||||
var low = Math.Max(1, averageSceneMinutes * remainingScenes);
|
||||
var high = Math.Max(low + 1, low * 2);
|
||||
return $"About {low:N0}-{high:N0} minutes";
|
||||
}
|
||||
=> model.HasActiveRuns && model.TotalCompletedScenes > 0 ? "Estimating..." : model.HasActiveRuns ? "Estimating..." : "Ready to review";
|
||||
|
||||
private static string CurrentScene(StoryIntelligenceProgressViewModel model)
|
||||
{
|
||||
|
||||
216
PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml
Normal file
216
PlotLine/Views/Onboarding/StoryIntelligenceReview.cshtml
Normal file
@ -0,0 +1,216 @@
|
||||
@model StoryIntelligenceProgressViewModel
|
||||
@using System.Security.Claims
|
||||
@inject IConfiguration Configuration
|
||||
@{
|
||||
ViewData["Title"] = "Review analysis";
|
||||
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 readyToCreate = Model.Chapters.Count(chapter => chapter.CanCommit);
|
||||
}
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="story-review-title">
|
||||
<div class="onboarding-panel onboarding-review-panel story-review-page">
|
||||
<div class="onboarding-copy">
|
||||
<p class="eyebrow">@Model.BookTitle</p>
|
||||
<h1 id="story-review-title">Review analysis</h1>
|
||||
<p>PlotDirector has finished reading your manuscript. Review the chapter results, then create scenes when you are ready.</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">
|
||||
Nothing is added to your project until you choose Create scenes. This step only creates scenes, summaries and supported scene notes.
|
||||
</section>
|
||||
|
||||
<section class="story-review-summary" aria-label="Analysis summary">
|
||||
<div>
|
||||
<span>Chapters analysed</span>
|
||||
<strong>@Model.CompletedChapterCount of @Model.ChapterCount</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scenes found</span>
|
||||
<strong>@Model.TotalDetectedScenes.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Ready to create</span>
|
||||
<strong>@readyToCreate.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Needs review</span>
|
||||
<strong>@chaptersNeedingReview.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))
|
||||
{
|
||||
<article class="story-review-chapter-card">
|
||||
<div class="story-review-chapter-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Chapter @chapter.ChapterNumber</p>
|
||||
<h2>@chapter.ChapterTitle</h2>
|
||||
</div>
|
||||
<strong class="@StatusClass(chapter)">@FriendlyStatus(chapter)</strong>
|
||||
</div>
|
||||
|
||||
<div class="story-review-scene-panel">
|
||||
<div>
|
||||
<span>Scenes found</span>
|
||||
<strong>@((chapter.TotalDetectedScenes ?? 0).ToString("N0"))</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scenes ready</span>
|
||||
<strong>@((chapter.CompletedScenes ?? 0).ToString("N0"))</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Needs review</span>
|
||||
<strong>@(((chapter.FailedScenes ?? 0) + chapter.Blockers.Count).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>@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>@blocker</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(chapter.ErrorMessage))
|
||||
{
|
||||
<p class="story-review-note story-review-note--warning">@chapter.ErrorMessage</p>
|
||||
}
|
||||
|
||||
<div class="story-review-card-actions">
|
||||
@if (chapter.HasCompletedCommit)
|
||||
{
|
||||
<button class="btn btn-outline-secondary" type="button" disabled>@chapter.ScenesCreated.ToString("N0") scene(s) created</button>
|
||||
}
|
||||
else if (chapter.CanCommit && Model.BatchID.HasValue)
|
||||
{
|
||||
<form asp-action="CommitStoryIntelligence" method="post">
|
||||
<input type="hidden" name="batchId" value="@Model.BatchID" />
|
||||
<input type="hidden" name="runId" value="@chapter.RunID" />
|
||||
<button class="btn btn-primary" type="submit">Create scenes</button>
|
||||
</form>
|
||||
}
|
||||
else if (chapter.IsActive && Model.BatchID.HasValue)
|
||||
{
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceProgress" asp-route-batchId="@Model.BatchID">View progress</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-outline-secondary" type="button" disabled>Needs attention</button>
|
||||
}
|
||||
</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>
|
||||
</details>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
</section>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
@if (Model.BatchID.HasValue)
|
||||
{
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceProgress" asp-route-batchId="@Model.BatchID">View progress</a>
|
||||
}
|
||||
<form asp-action="ScanManuscript" method="post">
|
||||
<button class="btn btn-outline-secondary" type="submit">Scan manuscript again</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@functions {
|
||||
private static bool NeedsReview(StoryIntelligenceOnboardingChapterViewModel chapter)
|
||||
=> chapter.IsFailed
|
||||
|| (chapter.FailedScenes ?? 0) > 0
|
||||
|| chapter.Blockers.Count > 0
|
||||
|| (!chapter.CanCommit && !chapter.HasCompletedCommit && chapter.IsRunComplete);
|
||||
|
||||
private static string FriendlyStatus(StoryIntelligenceOnboardingChapterViewModel chapter)
|
||||
{
|
||||
if (chapter.HasCompletedCommit)
|
||||
{
|
||||
return "Scenes created";
|
||||
}
|
||||
|
||||
if (chapter.CanCommit)
|
||||
{
|
||||
return chapter.Warnings.Count > 0 ? "Ready with notes" : "Ready to create";
|
||||
}
|
||||
|
||||
return chapter.Status switch
|
||||
{
|
||||
StoryIntelligenceRunStatuses.Pending => "Waiting",
|
||||
StoryIntelligenceRunStatuses.Running => "Reading",
|
||||
StoryIntelligenceRunStatuses.Completed => "Needs attention",
|
||||
StoryIntelligenceRunStatuses.CompletedWithWarnings => "Needs attention",
|
||||
StoryIntelligenceRunStatuses.Failed => "Needs review",
|
||||
StoryIntelligenceRunStatuses.Cancelled => "Cancelled",
|
||||
_ => "Needs attention"
|
||||
};
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
@ -596,6 +596,211 @@
|
||||
color: #1f2a44;
|
||||
}
|
||||
|
||||
.story-live-progress {
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.story-live-hero h1 {
|
||||
font-size: clamp(2.4rem, 5vw, 4rem);
|
||||
}
|
||||
|
||||
.story-live-progress-card,
|
||||
.story-live-current,
|
||||
.story-live-feed-grid,
|
||||
.story-review-summary,
|
||||
.story-review-chapter-card {
|
||||
border: 1px solid rgba(31, 42, 68, .12);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, .62);
|
||||
}
|
||||
|
||||
.story-live-progress-card {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
padding: 1.2rem;
|
||||
}
|
||||
|
||||
.story-live-progress-heading,
|
||||
.story-review-chapter-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.story-live-progress-heading strong {
|
||||
display: block;
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.story-live-progress-heading > span,
|
||||
.story-review-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 2rem;
|
||||
border-radius: 999px;
|
||||
padding: .25rem .7rem;
|
||||
background: rgba(47, 111, 99, .12);
|
||||
color: #2f6f63;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.story-live-stats,
|
||||
.story-review-summary,
|
||||
.story-review-scene-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: .75rem;
|
||||
}
|
||||
|
||||
.story-live-stats div,
|
||||
.story-review-summary div,
|
||||
.story-review-scene-panel div {
|
||||
min-width: 0;
|
||||
border: 1px solid rgba(31, 42, 68, .1);
|
||||
border-radius: 8px;
|
||||
padding: .8rem;
|
||||
background: rgba(255, 255, 255, .58);
|
||||
}
|
||||
|
||||
.story-live-stats span,
|
||||
.story-review-summary span,
|
||||
.story-review-scene-panel span {
|
||||
display: block;
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: .78rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.story-live-stats strong,
|
||||
.story-review-summary strong,
|
||||
.story-review-scene-panel strong {
|
||||
display: block;
|
||||
margin-top: .15rem;
|
||||
font-size: 1.12rem;
|
||||
}
|
||||
|
||||
.story-live-current,
|
||||
.story-live-feed-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(280px, .8fr);
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.story-live-current h2,
|
||||
.story-live-feed-grid h2,
|
||||
.story-review-chapter-card h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.story-live-feed-grid {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.story-review-page {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.story-review-summary {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.story-review-chapter-list {
|
||||
display: grid;
|
||||
gap: .9rem;
|
||||
}
|
||||
|
||||
.story-review-chapter-card {
|
||||
display: grid;
|
||||
gap: .9rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.story-review-status--created {
|
||||
background: rgba(47, 111, 99, .16);
|
||||
}
|
||||
|
||||
.story-review-status--ready {
|
||||
background: rgba(47, 111, 99, .12);
|
||||
}
|
||||
|
||||
.story-review-status--active {
|
||||
background: rgba(182, 138, 70, .14);
|
||||
color: #755624;
|
||||
}
|
||||
|
||||
.story-review-status--warning {
|
||||
background: rgba(176, 58, 46, .12);
|
||||
color: #8a2f25;
|
||||
}
|
||||
|
||||
.story-review-note {
|
||||
margin: 0;
|
||||
border-left: 4px solid rgba(47, 111, 99, .45);
|
||||
border-radius: 8px;
|
||||
padding: .8rem 1rem;
|
||||
background: rgba(47, 111, 99, .07);
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
.story-review-note ul {
|
||||
margin: .35rem 0 0;
|
||||
padding-left: 1.1rem;
|
||||
}
|
||||
|
||||
.story-review-note--warning {
|
||||
border-left-color: rgba(176, 58, 46, .5);
|
||||
background: rgba(176, 58, 46, .07);
|
||||
}
|
||||
|
||||
.story-review-card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: .65rem;
|
||||
}
|
||||
|
||||
.story-review-admin {
|
||||
border-top: 1px solid rgba(31, 42, 68, .1);
|
||||
padding-top: .75rem;
|
||||
}
|
||||
|
||||
.story-review-admin summary {
|
||||
cursor: pointer;
|
||||
color: var(--bs-secondary-color);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.story-review-admin dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: .6rem;
|
||||
margin: .75rem 0 0;
|
||||
}
|
||||
|
||||
.story-review-admin div {
|
||||
min-width: 0;
|
||||
border: 1px solid rgba(31, 42, 68, .1);
|
||||
border-radius: 8px;
|
||||
padding: .65rem;
|
||||
}
|
||||
|
||||
.story-review-admin dt {
|
||||
color: var(--bs-secondary-color);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.story-review-admin dd {
|
||||
margin: .15rem 0 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
[data-story-discovery-feed] {
|
||||
display: grid;
|
||||
gap: .45rem;
|
||||
@ -921,6 +1126,20 @@
|
||||
position: static;
|
||||
}
|
||||
|
||||
.story-live-stats,
|
||||
.story-review-summary,
|
||||
.story-review-scene-panel,
|
||||
.story-live-current,
|
||||
.story-live-feed-grid,
|
||||
.story-review-admin dl {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.story-live-progress-heading,
|
||||
.story-review-chapter-heading {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.onboarding-build-panel,
|
||||
.onboarding-success-heading {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@ -88,9 +88,12 @@
|
||||
};
|
||||
|
||||
const possibleDiscoveryText = (text) => String(text || "")
|
||||
.replace(/^Character detected:/i, "Possible character detected:")
|
||||
.replace(/^Location detected:/i, "Possible location detected:")
|
||||
.replace(/^Asset detected:/i, "Possible asset detected:")
|
||||
.replace(/^Character detected:/i, "Possible character found:")
|
||||
.replace(/^Possible character detected:/i, "Possible character found:")
|
||||
.replace(/^Location detected:/i, "Possible location found:")
|
||||
.replace(/^Possible location detected:/i, "Possible location found:")
|
||||
.replace(/^Asset detected:/i, "Possible asset found:")
|
||||
.replace(/^Possible asset detected:/i, "Possible asset found:")
|
||||
.replace(/^Timeline clue detected:/i, "Timeline clue found:")
|
||||
.replace(/^Relationship detected:/i, "Relationship signal found:");
|
||||
|
||||
@ -363,6 +366,10 @@
|
||||
if (summaryNode && latestSummary) {
|
||||
summaryNode.textContent = latestSummary;
|
||||
summaryNode.hidden = false;
|
||||
const emptyNode = root.querySelector("[data-story-latest-empty]");
|
||||
if (emptyNode) {
|
||||
emptyNode.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
const feed = root.querySelector("[data-story-discovery-feed]");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user