@model StoryIntelligenceSavedRunDetailViewModel @{ ViewData["Title"] = "Story Intelligence Run"; var run = Model.Run; var canCancel = run is not null && (run.Status == StoryIntelligenceRunStatuses.Pending || run.Status == StoryIntelligenceRunStatuses.Running); var shouldRefresh = run is not null && (canCancel || run.Status == "Running"); } @if (shouldRefresh) { }

Admin utility

Story Intelligence Run @(run is null ? string.Empty : run.StoryIntelligenceRunID.ToString("N0"))

Saved audit output from the admin chapter-to-scene dry-run pipeline.

@if (TempData["AdminMessage"] is string message) {
@message
} @if (run is null) {

This Story Intelligence run could not be found.

} else {

Run metadata

Status

@run.Status

Scenes

@Model.SceneResults.Count.ToString("N0")

Tokens

@Display(run.TotalTokens)

Duration

@DisplayDuration(run.TotalDurationMs)

Current stage
@Display(run.CurrentStage)
Current message
@Display(run.CurrentMessage)
Chapter structure status
@ChapterStructureStatus(Model.ChapterResult, run)
Scene progress
Detected: @Display(run.TotalDetectedScenes), completed: @Display(run.CompletedScenes), failed: @Display(run.FailedScenes)
Source type
@run.SourceType
Source file
@Display(run.SourceFileName)
Source file size
@DisplayBytes(run.SourceFileSizeBytes)
Source word count
@Display(run.SourceWordCount)
Source character count
@Display(run.SourceCharacterCount)
Submitted paragraph count
@Display(run.SourceParagraphCount)
Source chapter count
@Display(run.SourceChapterCount)
Detected document features
Images: @Display(run.SourceDetectedImagesCount), tables: @Display(run.SourceDetectedTablesCount), footnotes: @Display(run.SourceDetectedFootnotesCount), comments: @Display(run.SourceDetectedCommentsCount)
Project
@DisplayName(run.ProjectTitle, run.ProjectID, "Project")
Book
@DisplayName(run.BookTitle, run.BookID, "Book")
Chapter
@DisplayChapter(run.ChapterID, run.ChapterNumber)
Prompt version
@run.PromptVersion
Prompt versions summary
@Display(run.PromptVersionsSummary)
Model
@run.Model
Started
@run.StartedUtc.ToString("yyyy-MM-dd HH:mm:ss") UTC
Completed
@(run.CompletedUtc?.ToString("yyyy-MM-dd HH:mm:ss") ?? "None") UTC
Cancellation requested
@(run.CancellationRequestedUtc?.ToString("yyyy-MM-dd HH:mm:ss") ?? "None") UTC
Cancelled
@(run.CancelledUtc?.ToString("yyyy-MM-dd HH:mm:ss") ?? "None") UTC
Failure stage
@Display(run.FailureStage)
Input tokens
@Display(run.TotalInputTokens)
Output tokens
@Display(run.TotalOutputTokens)
Estimated cost
@DisplayCost(run.EstimatedCostGBP, "GBP") / @DisplayCost(run.EstimatedCostUSD, "USD")
Error message
@Display(run.ErrorMessage)
Error detail
@Display(run.ErrorDetail)
@if (canCancel) {
}

Chapter result

@if (Model.ChapterResult is null) {

No chapter result was saved for this run.

} else { var chapter = Model.ChapterResult;
Source label
@chapter.SourceLabel
Chapter
@Display(chapter.ChapterNumber)
Prompt version
@chapter.PromptVersion
Validation
@chapter.ValidationErrorsCount.ToString("N0") error(s), @chapter.ValidationWarningsCount.ToString("N0") warning(s)
Duration
@DisplayDuration(chapter.DurationMs)
Tokens
@Display(chapter.TotalTokens) total (@Display(chapter.InputTokens) input, @Display(chapter.OutputTokens) output)
Highest returned endParagraph
@Display(HighestReturnedEndParagraph(chapter))
Returned scene boundary ranges
@DisplayBoundaryRanges(chapter)
@if (ReturnedEndParagraphExceedsSubmitted(chapter, run)) {
The model returned paragraph numbers beyond the submitted chapter. This usually means the prompt was not using explicit numbered paragraphs.
} }

Scene results

@if (Model.SceneResults.Count == 0) {

No scene results were saved for this run.

}
@foreach (var scene in Model.SceneResults) {

Suggested Scene @scene.TemporarySceneNumber.ToString("N0")

Paragraph range
@Display(scene.StartParagraph)-@Display(scene.EndParagraph)
Source label
@scene.SourceLabel
Prompt version
@scene.PromptVersion
Validation
@scene.ValidationErrorsCount.ToString("N0") error(s), @scene.ValidationWarningsCount.ToString("N0") warning(s)
@if (IsFailedScene(scene)) {
Failure stage
@SceneFailureStage(scene)
Error message
@SceneErrorMessage(scene)
Retry attempted
No
}
Duration
@DisplayDuration(scene.DurationMs)
Tokens
@Display(scene.TotalTokens) total (@Display(scene.InputTokens) input, @Display(scene.OutputTokens) output)
} } @functions { private static string Display(object? value) => value switch { null => "None", string text when string.IsNullOrWhiteSpace(text) => "None", decimal number => number.ToString("0.##"), int number => number.ToString("N0"), _ => value.ToString() ?? "None" }; private static string DisplayDuration(long? milliseconds) => milliseconds.HasValue ? TimeSpan.FromMilliseconds(milliseconds.Value).TotalSeconds < 1 ? $"{milliseconds.Value:N0} ms" : $"{TimeSpan.FromMilliseconds(milliseconds.Value).TotalSeconds:0.00} sec" : "None"; private static string DisplayBytes(long? bytes) => bytes.HasValue ? $"{bytes.Value:N0} bytes" : "None"; private static string DisplayCost(decimal? value, string currency) => value.HasValue ? $"{value.Value:0.000000} {currency}" : "None"; private static string DisplayName(string? title, int? id, string label) => !string.IsNullOrWhiteSpace(title) ? title : id.HasValue ? $"{label} {id.Value:N0}" : "None"; private static string DisplayChapter(int? chapterId, decimal? chapterNumber) => chapterId.HasValue ? chapterNumber.HasValue ? $"Chapter {chapterNumber.Value:0.##} (ID {chapterId.Value:N0})" : $"Chapter ID {chapterId.Value:N0}" : "None"; private static string ChapterStructureStatus(StoryIntelligenceSavedChapterResult? chapter, StoryIntelligenceSavedRun run) { if (chapter is not null) { return chapter.ValidationErrorsCount > 0 ? $"Saved with {chapter.ValidationErrorsCount:N0} validation error(s)." : "Saved."; } return string.Equals(run.FailureStage, StoryIntelligenceFailureStages.ChapterStructure, StringComparison.Ordinal) ? "Failed before a valid chapter structure result was saved." : "Not saved yet."; } private static bool IsFailedScene(StoryIntelligenceSavedSceneResult scene) => scene.ValidationErrorsCount > 0 && string.IsNullOrWhiteSpace(scene.RawResponseJson) && !string.IsNullOrWhiteSpace(scene.ParsedJson); private static string SceneFailureStage(StoryIntelligenceSavedSceneResult scene) => SceneErrorMessage(scene).Contains("boundary", StringComparison.OrdinalIgnoreCase) ? StoryIntelligenceFailureStages.SceneSplit : StoryIntelligenceFailureStages.SceneIntelligence; private static string SceneErrorMessage(StoryIntelligenceSavedSceneResult scene) { try { using var document = System.Text.Json.JsonDocument.Parse(scene.ParsedJson); if (document.RootElement.TryGetProperty("error", out var error) && error.ValueKind == System.Text.Json.JsonValueKind.String) { return error.GetString() ?? "Scene failed."; } } catch (System.Text.Json.JsonException) { } return "Scene failed. See parsed JSON for details."; } private static int? HighestReturnedEndParagraph(StoryIntelligenceSavedChapterResult chapter) => ReadBoundaryRanges(chapter).Select(range => range.End).DefaultIfEmpty().Max() is var max && max > 0 ? max : null; private static string DisplayBoundaryRanges(StoryIntelligenceSavedChapterResult chapter) { var ranges = ReadBoundaryRanges(chapter).ToList(); return ranges.Count == 0 ? "None" : string.Join(", ", ranges.Select(range => $"{range.Start}-{range.End}")); } private static bool ReturnedEndParagraphExceedsSubmitted(StoryIntelligenceSavedChapterResult chapter, StoryIntelligenceSavedRun run) { var highest = HighestReturnedEndParagraph(chapter); var submitted = run.SourceParagraphCount; return highest.HasValue && submitted.HasValue && highest.Value > submitted.Value; } private static IReadOnlyList<(int Start, int End)> ReadBoundaryRanges(StoryIntelligenceSavedChapterResult chapter) { if (string.IsNullOrWhiteSpace(chapter.ParsedJson)) { return []; } try { using var document = System.Text.Json.JsonDocument.Parse(chapter.ParsedJson); if (!document.RootElement.TryGetProperty("sceneBoundaries", out var boundaries) || boundaries.ValueKind != System.Text.Json.JsonValueKind.Array) { return []; } var ranges = new List<(int Start, int End)>(); foreach (var boundary in boundaries.EnumerateArray()) { var start = boundary.TryGetProperty("startParagraph", out var startElement) && startElement.TryGetInt32(out var startValue) ? startValue : 0; var end = boundary.TryGetProperty("endParagraph", out var endElement) && endElement.TryGetInt32(out var endValue) ? endValue : 0; ranges.Add((start, end)); } return ranges; } catch (System.Text.Json.JsonException) { return []; } } }