PlotDirector/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml

234 lines
11 KiB
Plaintext

@model StoryIntelligenceProgressViewModel
@{
ViewData["Title"] = "Review scenes";
var runIds = string.Join(",", Model.Chapters.Select(chapter => chapter.RunID));
var progressPercent = ProgressPercent(Model);
}
<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-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 turning them into scenes you can review before creating them.</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>
}
<div class="onboarding-scan-counts onboarding-review-counts">
<div>
<span>Overall progress</span>
<strong data-story-overall-percent>@progressPercent%</strong>
</div>
<div>
<span>Chapters completed</span>
<strong><span data-story-chapters-completed>@Model.CompletedChapterCount</span> / @Model.ChapterCount</strong>
</div>
<div>
<span>Scenes analysed</span>
<strong><span data-story-scenes-completed>@Model.TotalCompletedScenes</span> / <span data-story-scenes-total>@Model.TotalDetectedScenes</span></strong>
</div>
<div>
<span>Failures</span>
<strong data-story-failures>@(Model.FailedChapterCount + Model.TotalFailedScenes)</strong>
</div>
</div>
<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>Reading now</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 stage</span>
<strong data-story-current-stage>@(CurrentChapter(Model)?.CurrentStage ?? "Queued")</strong>
</div>
<div>
<span>Elapsed</span>
<strong data-story-elapsed>@Elapsed(Model)</strong>
</div>
<div>
<span>Estimated remaining</span>
<strong data-story-remaining>Calculating</strong>
</div>
</div>
<p data-story-current-message>@(CurrentChapter(Model)?.CurrentMessage ?? "Waiting for analysis to begin.")</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">
<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>@chapter.Status</strong>
</div>
<div>
<span>Scenes</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>Failed</span>
<strong data-story-run-failed>@((chapter.FailedScenes ?? 0).ToString("N0"))</strong>
</div>
<div>
<span>Tokens</span>
<strong data-story-run-tokens>@(chapter.TotalTokens?.ToString("N0") ?? "-")</strong>
</div>
</div>
<p data-story-run-message>@(chapter.CurrentMessage ?? string.Empty)</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>
</details>
}
</div>
</div>
</section>
<div class="onboarding-actions">
@if (Model.BatchID.HasValue)
{
<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 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;
}
if (model.TotalDetectedScenes > 0)
{
return Math.Clamp((int)Math.Round(model.TotalCompletedScenes * 100m / Math.Max(1, model.TotalDetectedScenes)), 0, 100);
}
return Math.Clamp((int)Math.Round(model.CompletedChapterCount * 100m / Math.Max(1, model.ChapterCount)), 0, 100);
}
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";
}
}