414 lines
18 KiB
Plaintext
414 lines
18 KiB
Plaintext
@model StoryIntelligenceProgressViewModel
|
|
@using System.Security.Claims
|
|
@inject IConfiguration Configuration
|
|
@{
|
|
ViewData["Title"] = "Review scenes";
|
|
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));
|
|
}
|
|
|
|
<section class="onboarding-shell" aria-labelledby="story-progress-title">
|
|
<div class="onboarding-panel onboarding-review-panel"
|
|
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">
|
|
<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>
|
|
</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" 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>
|
|
|
|
<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>
|
|
</div>
|
|
<p data-story-current-message>@FriendlyMessage(CurrentChapter(Model))</p>
|
|
<p class="lead" data-story-latest-summary hidden></p>
|
|
<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="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"
|
|
asp-route-batchId="@Model.BatchID"
|
|
aria-disabled="@(!Model.AllCommitted)">Continue</a>
|
|
}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
@section Scripts {
|
|
<script src="~/js/story-intelligence-progress.js" asp-append-version="true"></script>
|
|
}
|
|
|
|
@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.Cancelled => "Cancelled",
|
|
_ => "Waiting"
|
|
};
|
|
|
|
private static string FriendlyStage(string? stage, string? status)
|
|
{
|
|
if (status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings)
|
|
{
|
|
return "Ready to review";
|
|
}
|
|
|
|
if (status is StoryIntelligenceRunStatuses.Failed)
|
|
{
|
|
return "Needs attention";
|
|
}
|
|
|
|
if (status is StoryIntelligenceRunStatuses.Cancelled)
|
|
{
|
|
return "Cancelled";
|
|
}
|
|
|
|
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",
|
|
_ => "Reading"
|
|
};
|
|
}
|
|
|
|
private static string FriendlyMessage(StoryIntelligenceOnboardingChapterViewModel? chapter)
|
|
{
|
|
if (chapter is null)
|
|
{
|
|
return "Waiting for analysis to begin.";
|
|
}
|
|
|
|
if (chapter.Status == StoryIntelligenceRunStatuses.Cancelled)
|
|
{
|
|
return "Analysis was cancelled. Any completed chapters remain available for review.";
|
|
}
|
|
|
|
if (chapter.Status == StoryIntelligenceRunStatuses.Failed)
|
|
{
|
|
return "This chapter needs attention 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.",
|
|
StoryIntelligenceFailureStages.Validation => "Checking the results.",
|
|
_ => chapter.IsRunComplete
|
|
? "This chapter is ready to review."
|
|
: "Waiting for analysis to begin."
|
|
};
|
|
}
|
|
|
|
private static StoryIntelligenceOnboardingChapterViewModel? CurrentChapter(StoryIntelligenceProgressViewModel model)
|
|
=> model.Chapters.FirstOrDefault(chapter => chapter.IsActive)
|
|
?? model.Chapters.LastOrDefault(chapter => chapter.IsRunComplete)
|
|
?? model.Chapters.FirstOrDefault();
|
|
|
|
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)
|
|
{
|
|
return 1m;
|
|
}
|
|
|
|
if (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;
|
|
}
|
|
|
|
private static string Elapsed(StoryIntelligenceProgressViewModel model)
|
|
{
|
|
var elapsedMs = model.Chapters.Max(chapter => chapter.TotalDurationMs ?? 0);
|
|
var elapsed = TimeSpan.FromMilliseconds(Math.Max(0, elapsedMs));
|
|
return elapsed.TotalMinutes >= 1
|
|
? $"{Math.Max(1, (int)Math.Round(elapsed.TotalMinutes)):N0} min"
|
|
: $"{Math.Max(0, (int)Math.Round(elapsed.TotalSeconds)):N0} sec";
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
private static string CurrentScene(StoryIntelligenceProgressViewModel model)
|
|
{
|
|
var chapter = CurrentChapter(model);
|
|
return chapter?.CompletedScenes is > 0
|
|
? chapter.CompletedScenes.Value.ToString("N0")
|
|
: "Waiting";
|
|
}
|
|
}
|