370 lines
16 KiB
Plaintext
370 lines
16 KiB
Plaintext
@model StoryIntelligenceProgressViewModel
|
||
@{
|
||
ViewData["Title"] = "Reading your manuscript";
|
||
var runIds = string.Join(",", Model.Chapters.Select(chapter => chapter.RunID));
|
||
var progressPercent = ProgressPercent(Model);
|
||
var currentChapter = CurrentChapter(Model);
|
||
}
|
||
|
||
<section class="onboarding-shell" aria-labelledby="story-progress-title">
|
||
<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()">
|
||
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Analyse Manuscript" })" />
|
||
<div class="onboarding-copy story-live-hero">
|
||
<p class="eyebrow">Analyse manuscript</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)
|
||
{
|
||
<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">
|
||
PlotDirector will keep analysing your manuscript. You can close Word, close this browser tab, and return later. We’ll show this import on your Projects page while it is running.
|
||
</section>
|
||
|
||
<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>
|
||
<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 class="story-stat-value" data-story-chapters-read-value>@Model.CompletedChapterCount of @Model.ChapterCount</strong>
|
||
</div>
|
||
<div>
|
||
<span>Scenes read</span>
|
||
<strong class="story-stat-value" data-story-scenes-read-value>@Model.TotalCompletedScenes so far</strong>
|
||
</div>
|
||
<div>
|
||
<span>Needs review</span>
|
||
<strong class="story-stat-value" data-story-failures>@(Model.FailedChapterCount + Model.TotalFailedScenes)</strong>
|
||
</div>
|
||
<div>
|
||
<span>Elapsed</span>
|
||
<strong class="story-stat-value" data-story-elapsed>@Elapsed(Model)</strong>
|
||
</div>
|
||
<div>
|
||
<span>Estimated remaining</span>
|
||
<strong class="story-stat-value" 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 class="story-current-scene-panel">
|
||
<p class="eyebrow">Current scene</p>
|
||
<strong data-story-current-scene>@CurrentScene(Model)</strong>
|
||
<div class="story-chapter-progress">
|
||
<span data-story-chapter-progress-text>@ChapterProgressText(currentChapter)</span>
|
||
<div class="progress" role="progressbar" aria-label="Current chapter progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="@ChapterProgressPercent(currentChapter)">
|
||
<div class="progress-bar" data-story-chapter-progress-bar style="width:@ChapterProgressPercent(currentChapter)%"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="story-live-feed-grid" aria-label="Live manuscript feed">
|
||
<div>
|
||
<h2>Latest from your manuscript</h2>
|
||
@if (!string.IsNullOrWhiteSpace(currentChapter?.LatestSceneSummary))
|
||
{
|
||
<p class="lead story-latest-summary" data-story-latest-summary>@currentChapter.LatestSceneSummary</p>
|
||
<p class="text-muted" data-story-latest-empty hidden>Scene summaries will appear here as PlotDirector reads.</p>
|
||
}
|
||
else
|
||
{
|
||
<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 class="story-live-discoveries">
|
||
<h2>Recent discoveries</h2>
|
||
<p class="text-muted story-discovery-placeholder" data-story-discovery-empty>Possible characters, locations and story clues will appear here as PlotDirector reads.</p>
|
||
<ul data-story-discovery-feed></ul>
|
||
</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"
|
||
data-story-chapter-number="@chapter.ChapterNumber"
|
||
data-story-current-stage="@(chapter.CurrentStage ?? string.Empty)"
|
||
data-story-current-scene-number="@(chapter.CompletedScenes ?? 0)"
|
||
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>@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>
|
||
<span data-story-run-latest-summary>@(chapter.LatestSceneSummary ?? string.Empty)</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<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>
|
||
@if (Model.HasActiveRuns)
|
||
{
|
||
<a class="btn btn-outline-primary" asp-controller="Projects" asp-action="Index">Continue in background</a>
|
||
}
|
||
@if (Model.ImportSessionID.HasValue)
|
||
{
|
||
<a class="btn btn-outline-primary"
|
||
href="/Development/StoryIntelligenceExperience?importSessionId=@Model.ImportSessionID.Value&mode=replay">Replay Story Intelligence</a>
|
||
}
|
||
@if (!Model.HasActiveRuns && Model.Chapters.Any(chapter => chapter.IsRunComplete || chapter.IsFailed || chapter.HasCompletedCommit))
|
||
{
|
||
<a class="btn btn-primary"
|
||
data-story-review-link
|
||
asp-action="StoryIntelligenceReview"
|
||
asp-route-batchId="@Model.BatchID">Review scenes</a>
|
||
}
|
||
}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
@section Scripts {
|
||
<script src="~/js/story-intelligence-progress.js" asp-append-version="true"></script>
|
||
}
|
||
|
||
@functions {
|
||
private static string FriendlyStatus(string? status)
|
||
=> status switch
|
||
{
|
||
StoryIntelligenceRunStatuses.Pending => "Waiting",
|
||
StoryIntelligenceRunStatuses.Running => "Reading",
|
||
StoryIntelligenceRunStatuses.Completed => "Ready to create",
|
||
StoryIntelligenceRunStatuses.CompletedWithWarnings => "Ready",
|
||
StoryIntelligenceRunStatuses.Failed => "Needs review",
|
||
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 review";
|
||
}
|
||
|
||
if (status is StoryIntelligenceRunStatuses.Cancelled)
|
||
{
|
||
return "Cancelled";
|
||
}
|
||
|
||
return stage switch
|
||
{
|
||
"Queued" => "Waiting",
|
||
StoryIntelligenceFailureStages.ChapterStructure => "Finding scenes",
|
||
StoryIntelligenceFailureStages.SceneIntelligence => "Reading scenes",
|
||
StoryIntelligenceFailureStages.SceneSplit => "Preparing review",
|
||
StoryIntelligenceFailureStages.Validation => "Preparing review",
|
||
StoryIntelligenceFailureStages.Persistence => "Preparing 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 review before scenes can be created.";
|
||
}
|
||
|
||
return chapter.CurrentStage switch
|
||
{
|
||
"Queued" => "Waiting to read 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 || 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 "Ready to review";
|
||
}
|
||
|
||
var completed = model.Chapters
|
||
.Where(chapter => chapter.IsRunComplete && (chapter.SourceWordCount ?? 0) > 0 && (chapter.TotalDurationMs ?? 0) > 0)
|
||
.ToList();
|
||
if (completed.Count == 0)
|
||
{
|
||
return "Estimating...";
|
||
}
|
||
|
||
var completedWords = completed.Sum(chapter => chapter.SourceWordCount ?? 0);
|
||
var completedSeconds = completed.Sum(chapter => chapter.TotalDurationMs ?? 0) / 1000m;
|
||
var remainingWords = model.Chapters
|
||
.Where(chapter => !chapter.IsRunComplete && !chapter.IsFailed)
|
||
.Sum(chapter => chapter.SourceWordCount ?? 0);
|
||
if (completedWords <= 0 || completedSeconds <= 0 || remainingWords <= 0)
|
||
{
|
||
return "Estimating...";
|
||
}
|
||
|
||
var secondsPerWord = completedSeconds / completedWords;
|
||
var bufferedSeconds = secondsPerWord * remainingWords * 1.35m;
|
||
return FriendlyRemaining(bufferedSeconds);
|
||
}
|
||
|
||
private static string FriendlyRemaining(decimal seconds)
|
||
{
|
||
var minutes = (int)Math.Ceiling(seconds / 60m);
|
||
if (minutes <= 0)
|
||
{
|
||
return "Estimating...";
|
||
}
|
||
|
||
if (minutes <= 20)
|
||
{
|
||
return "About 20-30 minutes";
|
||
}
|
||
|
||
if (minutes <= 45)
|
||
{
|
||
return "About 45-60 minutes";
|
||
}
|
||
|
||
if (minutes <= 120)
|
||
{
|
||
return "About 1-2 hours";
|
||
}
|
||
|
||
return "More than 2 hours";
|
||
}
|
||
|
||
private static string CurrentScene(StoryIntelligenceProgressViewModel model)
|
||
{
|
||
var chapter = CurrentChapter(model);
|
||
return chapter?.CompletedScenes is > 0
|
||
? chapter.CompletedScenes.Value.ToString("N0")
|
||
: "Waiting";
|
||
}
|
||
|
||
private static string ChapterProgressText(StoryIntelligenceOnboardingChapterViewModel? chapter)
|
||
{
|
||
if ((chapter?.TotalDetectedScenes ?? 0) <= 0)
|
||
{
|
||
return "Finding scenes...";
|
||
}
|
||
|
||
var completed = Math.Clamp(chapter?.CompletedScenes ?? 0, 0, chapter?.TotalDetectedScenes ?? 0);
|
||
return $"Scene {completed:N0} of {chapter?.TotalDetectedScenes:N0}";
|
||
}
|
||
|
||
private static int ChapterProgressPercent(StoryIntelligenceOnboardingChapterViewModel? chapter)
|
||
{
|
||
var total = chapter?.TotalDetectedScenes ?? 0;
|
||
if (total <= 0)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
var completed = Math.Clamp(chapter?.CompletedScenes ?? 0, 0, total);
|
||
return Math.Clamp((int)Math.Floor(completed * 100m / total), 0, 100);
|
||
}
|
||
}
|