@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() ?? []; var isAdmin = !string.IsNullOrWhiteSpace(userEmail) && adminEmails.Any(email => string.Equals(email, userEmail, StringComparison.OrdinalIgnoreCase)); }

@Model.BookTitle

Review scenes

PlotDirector is reading the approved chapters and preparing scene suggestions for you to review.

@if (TempData["OnboardingStoryIntelligenceError"] is string error) {
@error
} @if (TempData["OnboardingStoryIntelligenceMessage"] is string message) {
@message
}
@if (Model.Chapters.Any(chapter => chapter.CanCommit || chapter.HasCompletedCommit)) { Scenes can be created now. Characters, locations, assets and relationships remain suggestions for later review. } else { Nothing has been added to your project yet. PlotDirector is preparing suggestions. You will review them before anything is created. }
Import progress @progressPercent%
Chapters read @Model.CompletedChapterCount of @Model.ChapterCount
Scenes read @Model.TotalCompletedScenes so far
Needs review @(Model.FailedChapterCount + Model.TotalFailedScenes)

These are chapters or scenes PlotDirector could not fully analyse. You can review them after the run completes.

Latest from your manuscript

Current chapter @(CurrentChapter(Model)?.ChapterTitle ?? "Waiting")
Current scene @CurrentScene(Model)
Current stage @FriendlyStage(CurrentChapter(Model)?.CurrentStage, CurrentChapter(Model)?.Status)
Elapsed @Elapsed(Model)
Estimated remaining @EstimateRemaining(Model)

@FriendlyMessage(CurrentChapter(Model))

    Chapters

    @foreach (var chapter in Model.Chapters.OrderBy(chapter => chapter.ChapterNumber)) {
    @chapter.ChapterNumber. @chapter.ChapterTitle @ChapterStatusLabel(chapter)
    Status @FriendlyStatus(chapter.Status)
    Scenes detected @((chapter.CompletedScenes ?? 0).ToString("N0")) / @((chapter.TotalDetectedScenes ?? 0).ToString("N0"))
    Needs review @((chapter.FailedScenes ?? 0).ToString("N0"))

    @FriendlyMessage(chapter)

    @if ((chapter.FailedScenes ?? 0) > 0) {

    Needs review after analysis completes.

    } else { } @if (!string.IsNullOrWhiteSpace(chapter.ErrorMessage)) {

    @chapter.ErrorMessage

    } else { } @if (chapter.Warnings.Any()) {
      @foreach (var warning in chapter.Warnings) {
    • @warning
    • }
    } @if (chapter.Blockers.Any()) {
      @foreach (var blocker in chapter.Blockers) {
    • @blocker
    • }
    }
    @if (chapter.HasCompletedCommit) { @chapter.ScenesCreated scene(s) created } else if (chapter.CanCommit && Model.BatchID.HasValue) {
    } else if (chapter.IsRunComplete) { Review needed }
    @if (isAdmin) {
    Diagnostics
    Run ID
    @chapter.RunID
    Raw status
    @chapter.Status
    Raw stage
    @(chapter.CurrentStage ?? "-")
    Tokens
    @(chapter.TotalTokens?.ToString("N0") ?? "-")
    @if (!string.IsNullOrWhiteSpace(chapter.FailureStage)) {
    Failure stage
    @chapter.FailureStage
    }
    }
    }
    @if (Model.BatchID.HasValue) {
    Continue }
    @section Scripts { } @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"; } }